import { left } from 'fp-ts/lib/Either' ; import { constFalse , constTrue , Lazy } from 'fp-ts/lib/function' ; import * as IO from 'fp-ts/lib/IO' ; import { IORef , newIORef } from 'fp-ts/lib/IORef' ; import { pipe } from 'fp-ts/lib/pipeable' ; import { State } from 'fp-ts/lib/State' ; import * as TE from 'fp-ts/lib/TaskEither' ; import { breakerClosed , breakerOpen , getCurrentTime } from './helpers' ; import { BreakerEnvironment , BreakerOptions , BreakerState , CircuitBreaker } from './types' ; /**
 * Default circuit breaker options
 */ export const defaultBreakerOptions = { maxBreakerFailures : 3 , resetTimeoutSecs : 60 , breakerDescription : 'Circuit breaker is open' , } ; export const circuitBreaker = < T > ( ) : CircuitBreaker < T > => ( opts ) => { const failingCall = ( ) : TE . TaskEither < Error , T > => TE . fromEither ( left ( new Error ( opts . breakerDescription ) ) ) ; const incErrors = ( ref ) => pipe ( getCurrentTime ( ) . read , IO . chain ( ( currentTime ) => pipe ( ref . read , IO . chain ( ( state ) => { switch ( state . tag ) { case 'Closed' : { const errorCount = state . errorCount ; if ( errorCount >= opts . maxBreakerFailures ) { return ref . write ( breakerOpen ( currentTime + ( opts . resetTimeoutSecs * 1000 ) ) ) ; } else { return ref . write ( breakerClosed ( errorCount + 1 ) ) ; } } case 'Open' : { return IO . of < void > ( undefined ) ; } } } ) ) ) ) ; const callIfClosed = ( request , ref ) : TE . TaskEither < Error , T > => TE . tryCatch ( request , ( reason ) => pipe ( incErrors ( ref ) , IO . map < void , Error > ( ( ) => ( reason instanceof Error ) ? reason : new Error ( String ( reason ) ) ) ) ( ) ) ; const canaryCall = ( request , ref ) : TE . TaskEither < Error , T > => pipe ( callIfClosed ( request , ref ) , TE . chain ( ( result ) => pipe ( ref . write ( breakerClosed ( 0 ) ) , IO . chain ( ( ) => IO . of ( result ) ) , ( a ) => TE . taskEither . fromIO ( a ) ) ) ) ; const callIfOpen = ( request , ref ) : TE . TaskEither < Error , T > => pipe ( getCurrentTime ( ) . read , IO . chain ( ( currentTime ) => pipe ( ref . read , IO . chain ( ( state ) => { switch ( state . tag ) { case 'Closed' : return pipe ( ref . write ( state ) , IO . map ( constFalse ) ) ; case 'Open' : { if ( currentTime > state . openEndTime ) { return pipe ( ref . write ( breakerOpen ( currentTime + ( opts . resetTimeoutSecs * 1000 ) ) ) , IO . map ( constTrue ) ) ; } return pipe ( ref . write ( state ) , IO . map ( constFalse ) ) ; } } } ) ) ) , ( a ) => TE . taskEither . fromIO < Error , boolean > ( a ) , TE . chain ( ( canaryRequest ) => canaryRequest ? canaryCall ( request , ref ) : failingCall ( ) ) ) ; const breakerService = ( { breakerState , request } ) => [ pipe ( breakerState . read , ( a ) => TE . taskEither . fromIO < Error , BreakerState > ( a ) , TE . chain ( ( state ) => { switch ( state . tag ) { case 'Closed' : return callIfClosed ( request , breakerState ) ; case 'Open' : return callIfOpen ( request , breakerState ) ; } } ) ) , { breakerState , request } , ] ; return breakerService ; } ; param opts export const singletonBreaker = ( opts ) => { const breakerState = newIORef ( breakerClosed ( 0 ) ) ( ) ; return < T > ( request ) : TE . TaskEither < Error , T > => circuitBreaker < T > ( ) ( opts ) ( { request , breakerState } ) [ 0 ] ; } ;