import { fetch as crossFetch } from 'cross-fetch' ; import { fold } from 'fp-ts/lib/Either' ; import { flow , unsafeCoerce } from 'fp-ts/lib/function' ; import { none , Option , some } from 'fp-ts/lib/Option' ; import { pipe } from 'fp-ts/lib/pipeable' ; import { TaskEither , tryCatch } from 'fp-ts/lib/TaskEither' ; import * as io from 'io-ts' ; import { HandlerNotSetError , JsonDeserializationError } from './errors' ; /**
 * Result of a fetch request – basically, a pair of code and payload
 */ export type Result < Code extends number , A > = { code : Code ; payload : A ; } ; export type Extractor < TResult , Code extends number > = ( response : Response ) => Promise < Data < TResult , Code > > ; type Handled < T , Code extends number > = T extends Result < infer C , infer D > ? C extends Code ? never : Result < C , D > : never ; type Data < T , Code extends number > = T extends Result < infer C , infer D > ? C extends Code ? D : never : never ; type HandlersMap < TResult extends Result < any , any > , To > = Map < TResult [ 'code' ] , [ ( data : Data < TResult , TResult [ 'code' ] > ) => To , io . Type < Data < TResult , TResult [ 'code' ] > > | undefined , Extractor < TResult , TResult [ 'code' ] > ] > ; /**
 * Fetch type – just for convenience
 */ export type Fetch = typeof fetch ; export const defaultExtractor = ( response ) => { const contentType = response . headers . get ( 'content-type' ) ; return contentType ?. includes ( 'application/json' ) ? response . json ( ) : response . text ( ) ; } ; export const jsonExtractor = ( response ) => response . json ( ) ; export const textExtractor = ( response ) => response . text ( ) ; export class template TResult see Result template To example export class Fetcher < TResult extends Result < any , any > , To > { private readonly handlers : HandlersMap < TResult , To > = new Map ( ) ; private restHandler ? : ( ) => To = void 0 ; param { RequestInfo } input see Request param { RequestInit } init param { Fetch } fetch memberof constructor ( private readonly input , private readonly init ? , private readonly fetch = crossFetch ) { } template B param { ( a : To ) => B } f returns { Fetcher < TResult , B > } memberof map < B > ( f ) { for ( const [ code , [ handler , codec , extractor ] ] of this . handlers ) { this . handlers . set ( code , unsafeCoerce ( [ flow ( handler , f ) , codec , extractor ] ) ) ; } return unsafeCoerce ( this ) ; } template Code param { Code } code see Fetcher param { ( data : Data < TResult , Code > ) => To } handler param { io . Type < Data < TResult , Code > > } codec returns { Fetcher < Handled < TResult , Code > , To > } memberof handle < Code extends TResult [ 'code' ] > ( code , handler , codec ? , extractor = defaultExtractor ) { this . handlers . set ( code , [ handler , codec , extractor ] ) ; return unsafeCoerce ( this ) ; } param { ( ) => To } restHandler returns { Fetcher < Handled < TResult , never > , To > } memberof discardRest ( restHandler ) { this . restHandler = restHandler ; return unsafeCoerce ( this ) ; } returns { TaskEither < Error , [ To , Option < io . Errors > ] > } memberof toTaskEither ( ) { return tryCatch ( ( ) => this . run ( ) , ( reason ) => reason instanceof Error ? reason : new Error ( `Something went wrong, details: ${ reason }` ) ) ; } returns { Promise < [ To , Option < io . Errors > ] > } memberof async run ( ) { try { const response = await this . fetch ( this . input , this . init ) ; const status = response . status as TResult [ 'code' ] ; const triplet = this . handlers . get ( status ) ; if ( triplet != null ) { const [ handler , codec , extractor ] = triplet ; try { const body = await extractor ( response ) ; try { if ( codec ) { return pipe ( codec . decode ( body ) , fold ( ( errors ) => [ handler ( body ) , some ( errors ) ] , ( res ) => [ handler ( res ) , none ] ) ) ; } return [ handler ( body ) , none ] ; } catch ( error ) { return Promise . reject ( new Error ( `Handler side error, details: ${ error }` ) ) ; } } catch ( jsonError ) { return Promise . reject ( new JsonDeserializationError ( `Could not deserialize response JSON, details: ${ jsonError }` ) ) ; } } if ( this . restHandler != null ) { return [ this . restHandler ( ) , none ] ; } return Promise . reject ( new HandlerNotSetError ( `Neither handler for ${ status } nor rest handler are set - consider adding \`.handle(${ status }, ...)\` or \`.discardRest(() => ...)\` calls to the chain` ) ) ; } catch ( error ) { return Promise . reject ( error ) ; } } }