import { request } from "https" ; import { IncomingHttpHeaders , RequestOptions } from "http" ; import { pipeline } from "stream" ; import { TextDecoder } from "util" ; import { safeJsonParse , Json } from "./safe-json-parse.js" ; import { FetchError , NonRetryableFetchError } from "./error.js" ; /** Interface for JS objects that can be used with the SimpleJsonFetcher */ export interface JsonFetcher < ResultType extends Json = Json > { fetch : ( uri : string , requestOptions ? : FetchRequestOptions , data ? : Buffer ) => Promise < ResultType > ; } /**
 * Interface for Request Options, that adds one additional option to the Node.js standard RequestOptions,
 * "responseTimeout", with which a timeout can be set within which the response must be received.
 * (Note the "timeout" in the Node.js standard RequestOptions, concerns something else: the socket idle timeout)
 */ type FetchRequestOptions = RequestOptions & { responseTimeout ? : number ; } ; param defaultRequestOptions export class SimpleJsonFetcher implements JsonFetcher { defaultRequestOptions : FetchRequestOptions ; constructor ( props ? ) { this . defaultRequestOptions = { timeout : 500 , responseTimeout : 1500 , ... props ?. defaultRequestOptions , } ; } param uri param requestOptions param data returns public async fetch < ResultType extends Json > ( uri , requestOptions ? , data ? ) { requestOptions = { ... this . defaultRequestOptions , ... requestOptions } ; try { return await fetchJson < ResultType > ( uri , requestOptions , data ) ; } catch ( err ) { if ( err instanceof NonRetryableFetchError ) { throw err ; } return fetchJson < ResultType > ( uri , requestOptions , data ) ; } } } param uri param requestOptions param data returns export async function fetchJson < ResultType extends Json > ( uri , requestOptions ? , data ? ) { let responseTimeout ; return new Promise < ResultType > ( ( resolve , reject ) => { const req = request ( uri , { method : "GET" , ... requestOptions , } , ( response ) => { ( pipeline as any ) ( [ response , getJsonDestination ( uri , response . statusCode , response . headers ) , ] , done ) ; } ) ; if ( requestOptions ?. responseTimeout ) { responseTimeout = setTimeout ( ( ) => done ( new FetchError ( uri , `Response time-out (after ${ requestOptions . responseTimeout } ms.)` ) ) , requestOptions . responseTimeout ) ; responseTimeout . unref ( ) ; } function done ( ... args ) { if ( responseTimeout ) clearTimeout ( responseTimeout ) ; if ( args [ 0 ] == null ) { resolve ( args [ 1 ] ) ; return ; } req . socket ?. emit ( "agentRemove" ) ; let error = args [ 0 ] ; if ( ! ( error instanceof FetchError ) ) { error = new FetchError ( uri , error . message ) ; } req . destroy ( ) ; reject ( error ) ; } req . on ( "error" , done ) ; req . end ( data ) ; } ) ; } param uri param statusCode param headers returns function getJsonDestination ( uri , statusCode , headers ) { return async ( responseIterable ) => { if ( statusCode === 429 ) { throw new FetchError ( uri , "Too many requests" ) ; } else if ( statusCode !== 200 ) { throw new NonRetryableFetchError ( uri , `Status code is ${ statusCode }, expected 200` ) ; } if ( ! headers [ "content-type" ] ?. toLowerCase ( ) . startsWith ( "application/json" ) ) { throw new NonRetryableFetchError ( uri , `Content-type is "${ headers [ "content-type" ] }", expected "application/json"` ) ; } const collected = [ ] as Buffer [ ] ; for await ( const chunk of responseIterable ) { collected . push ( chunk ) ; } try { return safeJsonParse ( new TextDecoder ( "utf8" , { fatal : true , ignoreBOM : true } ) . decode ( Buffer . concat ( collected ) ) ) ; } catch ( err ) { throw new NonRetryableFetchError ( uri , err ) ; } } ; }