import { delay , obscureAuthorizationHeader } from "./utils" ; import { NetworkTimeoutError , ServerResponse , UnparseableResponseError , ServerResponseObject , } from "./errors" ; import { Emitter , FetchFunction , FetchHeaders , FetchResponse } from "./types" ; interface HttpOptions { timeout ? : number | null ; requestMode ? : RequestMode ; fetchFunc ? : FetchFunction ; } interface RequestOptions { retry : number ; } export interface HttpResponse < T > { status : number ; json : T ; headers : FetchHeaders ; } private export default class HTTP { type { Object } static get DEFAULT_REQUEST_HEADERS ( ) : Record < string , string > { return { Accept : "application/json" , "Content-Type" : "application/json" , } ; } type { Object } static get defaultOptions ( ) : HttpOptions { return { timeout : null , requestMode : "cors" } ; } public events ? : Emitter ; public requestMode : RequestMode ; public timeout : number ; public fetchFunc : FetchFunction ; param { EventEmitter } events param options param { Number } options . timeout param { String } options . requestMode constructor ( events ? , options = { } ) { type { EventEmitter } this . events = events ; see https type { String } this . requestMode = options . requestMode || HTTP . defaultOptions . requestMode ! ; type { Number } this . timeout = options . timeout || HTTP . defaultOptions . timeout ! ; type { Function } this . fetchFunc = options . fetchFunc || globalThis . fetch . bind ( globalThis ) ; } private timedFetch ( url , options ) { let hasTimedout = false ; return new Promise ( ( resolve , reject ) => { let _timeoutId ; if ( this . timeout ) { _timeoutId = setTimeout ( ( ) => { hasTimedout = true ; if ( options && options . headers ) { options = { ... options , headers : obscureAuthorizationHeader ( options . headers ) , } ; } reject ( new NetworkTimeoutError ( url , options ) ) ; } , this . timeout ) ; } function proceedWithHandler ( fn ) { return ( arg ) => { if ( ! hasTimedout ) { if ( _timeoutId ) { clearTimeout ( _timeoutId ) ; } fn ( arg ) ; } } ; } this . fetchFunc ( url , options ) . then ( proceedWithHandler ( resolve ) ) . catch ( proceedWithHandler ( reject ) ) ; } ) ; } private async processResponse < T > ( response ) { const { status , headers } = response ; const text = await response . text ( ) ; let json ; if ( text . length !== 0 ) { try { json = JSON . parse ( text ) ; } catch ( err ) { throw new UnparseableResponseError ( response , text , err as Error ) ; } } if ( status >= 400 ) { throw new ServerResponse ( response , json as ServerResponseObject ) ; } return { status , json : json as T , headers } ; } private async retry < T > ( url , retryAfter , request , options ) { await delay ( retryAfter ) ; return this . request < T > ( url , request , { ... options , retry : options . retry - 1 , } ) ; } param { String } url param request param { Object } request . headers param options param { Number } options . retry return { Promise } async request < T > ( url , request = { headers : { } } , options = { retry : 0 } ) { request . headers = { ... HTTP . DEFAULT_REQUEST_HEADERS , ... request . headers } ; if ( request . body && request . body instanceof FormData ) { if ( request . headers instanceof Headers ) { request . headers . delete ( "Content-Type" ) ; } else if ( ! Array . isArray ( request . headers ) ) { delete request . headers [ "Content-Type" ] ; } } request . mode = this . requestMode ; const response = await this . timedFetch ( url , request ) ; const { headers } = response ; this . _checkForDeprecationHeader ( headers ) ; this . _checkForBackoffHeader ( headers ) ; const retryAfter = this . _checkForRetryAfterHeader ( headers ) ; if ( retryAfter && options . retry > 0 ) { return this . retry < T > ( url , retryAfter , request , options ) ; } else { return this . processResponse < T > ( response ) ; } } _checkForDeprecationHeader ( headers ) { const alertHeader = headers . get ( "Alert" ) ; if ( ! alertHeader ) { return ; } let alert ; try { alert = JSON . parse ( alertHeader ) ; } catch ( err ) { console . warn ( "Unable to parse Alert header message" , alertHeader ) ; return ; } console . warn ( alert . message , alert . url ) ; if ( this . events ) { this . events . emit ( "deprecated" , alert ) ; } } _checkForBackoffHeader ( headers ) { let backoffMs ; const backoffHeader = headers . get ( "Backoff" ) ; const backoffSeconds = backoffHeader ? parseInt ( backoffHeader , 10 ) : 0 ; if ( backoffSeconds > 0 ) { backoffMs = new Date ( ) . getTime ( ) + backoffSeconds * 1000 ; } else { backoffMs = 0 ; } if ( this . events ) { this . events . emit ( "backoff" , backoffMs ) ; } } _checkForRetryAfterHeader ( headers ) { const retryAfter = headers . get ( "Retry-After" ) ; if ( ! retryAfter ) { return ; } const delay = parseInt ( retryAfter , 10 ) * 1000 ; const tryAgainAfter = new Date ( ) . getTime ( ) + delay ; if ( this . events ) { this . events . emit ( "retry-after" , tryAgainAfter ) ; } return delay ; } }