function deepEqual ( a , b ) { if ( typeof a === "object" && typeof b === "object" ) { const aKeys = Object . keys ( a ) ; for ( const key of aKeys ) { if ( ! deepEqual ( a [ key ] , b [ key ] ) ) { return false ; } } return true ; } else if ( typeof a === "function" && typeof b === "function" ) { return true ; } else { return a === b ; } } type TestValue < A > = { value : A ; } | { error : any ; } ; type TestResult < A > = [ TestValue < A > , number ] ; export abstract class IO < A > { abstract run ( ) ; abstract test ( mocks , idx ) ; static of < A > ( a ) { return new PureIO ( a ) ; } of < A > ( a ) { return new PureIO ( a ) ; } map < B > ( f ) { return new FlatMapIO ( this , ( a ) => IO . of ( f ( a ) ) ) ; } chain < B > ( f ) { return new FlatMapIO ( this , f ) ; } flatMap < B > ( f ) { return new FlatMapIO ( this , f ) ; } } class PureIO < A > extends IO < A > { constructor ( private readonly a ) { super ( ) ; } run ( ) { return Promise . resolve ( this . a ) ; } test ( _mocks , idx ) { return [ { value : this . a } , idx ] ; } } class FlatMapIO < A , B > extends IO < B > { constructor ( private readonly io , private readonly f ) { super ( ) ; } run ( ) { return this . io . run ( ) . then ( ( a ) => this . f ( a ) . run ( ) ) ; } test ( mocks , idx ) { const [ value , newIdx ] = this . io . test ( mocks , idx ) ; if ( "value" in value ) { return this . f ( value . value ) . test ( mocks , newIdx ) ; } else { return [ value , newIdx ] ; } } } export function map < A , B > ( f , io ) { return new FlatMapIO ( io , ( a ) => IO . of ( f ( a ) ) ) ; } class CallPromiseIO < A , P extends any [ ] > extends IO < A > { constructor ( private readonly f , private readonly args ) { super ( ) ; } run ( ) { return this . f ( ... this . args ) ; } test ( mocks , idx ) { if ( ! deepEqual ( this , mocks [ idx ] [ 0 ] ) ) { throw new Error ( `Value invalid, expected ${ mocks [ idx ] [ 0 ] } but saw ${ this }` ) ; } return [ { value : mocks [ idx ] [ 1 ] } , idx + 1 ] ; } } export function withEffects < A , P extends any [ ] > ( f ) { return ( ... args ) => new CallPromiseIO ( ( ... a ) => Promise . resolve ( f ( ... a ) ) , args ) ; } export function withEffectsP < A , P extends any [ ] > ( f ) { return ( ... args ) => new CallPromiseIO ( f , args ) ; } export function call < A , P extends any [ ] > ( f , ... args ) { return new CallPromiseIO ( ( ... a ) => Promise . resolve ( f ( ... a ) ) , args ) ; } export function callP < A , P extends any [ ] > ( f , ... args ) { return new CallPromiseIO ( f , args ) ; } class ThrowErrorIO extends IO < any > { constructor ( private readonly error ) { super ( ) ; } run ( ) { return Promise . reject ( this . error ) ; } test ( _mocks , idx ) { return [ { error : this . error } , idx ] ; } } export function throwE ( error ) { return new ThrowErrorIO ( error ) ; } class CatchErrorIO < A , B > extends IO < A | B > { constructor ( private readonly io , private readonly errorHandler ) { super ( ) ; } run ( ) { return this . io . run ( ) . catch ( ( err ) => this . errorHandler ( err ) . run ( ) ) ; } test ( mocks , idx ) { const [ value , nextIdx ] = this . io . test ( mocks , idx ) ; if ( "value" in value ) { return [ value , nextIdx ] ; } else { return this . errorHandler ( value . error ) . test ( mocks , nextIdx ) ; } } } export function catchE ( errorHandler , io ) { return new CatchErrorIO ( io , errorHandler ) ; } export function runIO < A > ( e ) { return e . run ( ) ; } export function testIO < A > ( io , mocks , expectedResult ) { const [ value ] = io . test ( mocks , 0 ) ; if ( "value" in value ) { if ( ! deepEqual ( value . value , expectedResult ) ) { throw new Error ( `Value invalid, expected ${ expectedResult } but saw ${ value . value }` ) ; } } }