license import { Agent , AgentOptions as HttpsAgentOptions } from 'https' ; import { AgentOptions as HttpAgentOptions } from 'http' ; import fetch , * as f from 'node-fetch' ; import { PassThrough , Readable } from 'stream' ; import * as uuid from 'uuid' ; import { getAgent } from './agents' ; import { TeenyStatistics } from './TeenyStatistics' ; const streamEvents = require ( 'stream-events' ) ; export interface CoreOptions { method ? : string ; timeout ? : number ; gzip ? : boolean ; json ? : any ; headers ? : Headers ; body ? : string | { } ; useQuerystring ? : boolean ; qs ? : any ; proxy ? : string ; multipart ? : RequestPart [ ] ; forever ? : boolean ; pool ? : HttpsAgentOptions | HttpAgentOptions ; } export interface OptionsWithUri extends CoreOptions { uri : string ; } export interface OptionsWithUrl extends CoreOptions { url : string ; } export type Options = OptionsWithUri | OptionsWithUrl ; export interface Request extends PassThrough { agent : Agent | false ; headers : Headers ; href ? : string ; } export interface Response < T = any > { statusCode : number ; headers : Headers ; body : T ; request : Request ; statusMessage ? : string ; } export interface RequestPart { body : string | Readable ; } export interface RequestCallback < T = any > { ( err , response , body ? ) : void ; } export class RequestError extends Error { code ? : number ; } interface Headers { [ index ] : any ; } private param reqOpts function requestToFetchOptions ( reqOpts ) { const options = { method : reqOpts . method || 'GET' , ... ( reqOpts . timeout && { timeout : reqOpts . timeout } ) , ... ( typeof reqOpts . gzip === 'boolean' && { compress : reqOpts . gzip } ) , } ; if ( typeof reqOpts . json === 'object' ) { reqOpts . headers = reqOpts . headers || { } ; reqOpts . headers [ 'Content-Type' ] = 'application/json' ; options . body = JSON . stringify ( reqOpts . json ) ; } else { if ( Buffer . isBuffer ( reqOpts . body ) ) { options . body = reqOpts . body ; } else if ( typeof reqOpts . body !== 'string' ) { options . body = JSON . stringify ( reqOpts . body ) ; } else { options . body = reqOpts . body ; } } options . headers = reqOpts . headers as any ; let uri = ( ( reqOpts as OptionsWithUri ) . uri || ( reqOpts as OptionsWithUrl ) . url ) as string ; if ( ! uri ) { throw new Error ( 'Missing uri or url in reqOpts.' ) ; } if ( reqOpts . useQuerystring === true || typeof reqOpts . qs === 'object' ) { const qs = require ( 'querystring' ) ; const params = qs . stringify ( reqOpts . qs ) ; uri = uri + '?' + params ; } options . agent = getAgent ( uri , reqOpts ) ; return { uri , options } ; } private param opts param res returns function fetchToRequestResponse ( opts , res ) { const request = { } as Request ; request . agent = ( opts . agent as Agent ) || false ; request . headers = ( opts . headers || { } ) as Headers ; request . href = res . url ; const resHeaders = { } as Headers ; res . headers . forEach ( ( value , key ) => ( resHeaders [ key ] = value ) ) ; const response = Object . assign ( res . body , { statusCode : res . status , statusMessage : res . statusText , request , body : res . body , headers : resHeaders , toJSON : ( ) => ( { headers : resHeaders } ) , } ) ; return response as Response ; } private param boundary param multipart function createMultipartStream ( boundary , multipart ) { const finale = `--${ boundary }--` ; const stream = new PassThrough ( ) ; for ( const part of multipart ) { const preamble = `--${ boundary }\r\nContent-Type: ${ ( part as { [ 'Content-Type' ] ? : string ; } ) [ 'Content-Type' ] }\r\n\r\n` ; stream . write ( preamble ) ; if ( typeof part . body === 'string' ) { stream . write ( part . body ) ; stream . write ( '\r\n' ) ; } else { part . body . pipe ( stream , { end : false } ) ; part . body . on ( 'end' , ( ) => { stream . write ( '\r\n' ) ; stream . write ( finale ) ; stream . end ( ) ; } ) ; } } return stream ; } function teenyRequest ( reqOpts ) ; function teenyRequest ( reqOpts , callback ) ; function teenyRequest ( reqOpts , callback ? ) { const { uri , options } = requestToFetchOptions ( reqOpts ) ; const multipart = reqOpts . multipart as RequestPart [ ] ; if ( reqOpts . multipart && multipart . length === 2 ) { if ( ! callback ) { throw new Error ( 'Multipart without callback is not implemented.' ) ; } const boundary = uuid . v4 ( ) ; ( options . headers as Headers ) [ 'Content-Type' ] = `multipart/related; boundary=${ boundary }` ; options . body = createMultipartStream ( boundary , multipart ) ; teenyRequest . stats . requestStarting ( ) ; fetch ( uri , options ) . then ( res => { teenyRequest . stats . requestFinished ( ) ; const header = res . headers . get ( 'content-type' ) ; const response = fetchToRequestResponse ( options , res ) ; const body = response . body ; if ( header === 'application/json' || header === 'application/json; charset=utf-8' ) { res . json ( ) . then ( json => { response . body = json ; callback ( null , response , json ) ; } , ( err ) => { callback ( err , response , body ) ; } ) ; return ; } res . text ( ) . then ( text => { response . body = text ; callback ( null , response , text ) ; } , err => { callback ( err , response , body ) ; } ) ; } , err => { teenyRequest . stats . requestFinished ( ) ; callback ( err , null ! , null ) ; } ) ; return ; } if ( callback === undefined ) { const requestStream = streamEvents ( new PassThrough ( ) ) ; let responseStream ; requestStream . once ( 'reading' , ( ) => { if ( responseStream ) { responseStream . pipe ( requestStream ) ; } else { requestStream . once ( 'response' , ( ) => { responseStream . pipe ( requestStream ) ; } ) ; } } ) ; options . compress = false ; teenyRequest . stats . requestStarting ( ) ; fetch ( uri , options ) . then ( res => { teenyRequest . stats . requestFinished ( ) ; responseStream = res . body ; responseStream . on ( 'error' , ( err ) => { requestStream . emit ( 'error' , err ) ; } ) ; const response = fetchToRequestResponse ( options , res ) ; requestStream . emit ( 'response' , response ) ; } , err => { teenyRequest . stats . requestFinished ( ) ; requestStream . emit ( 'error' , err ) ; } ) ; return requestStream as Request ; } teenyRequest . stats . requestStarting ( ) ; fetch ( uri , options ) . then ( res => { teenyRequest . stats . requestFinished ( ) ; const header = res . headers . get ( 'content-type' ) ; const response = fetchToRequestResponse ( options , res ) ; const body = response . body ; if ( header === 'application/json' || header === 'application/json; charset=utf-8' ) { if ( response . statusCode === 204 ) { callback ( null , response , body ) ; return ; } res . json ( ) . then ( json => { response . body = json ; callback ( null , response , json ) ; } , err => { callback ( err , response , body ) ; } ) ; return ; } res . text ( ) . then ( text => { const response = fetchToRequestResponse ( options , res ) ; response . body = text ; callback ( null , response , text ) ; } , err => { callback ( err , response , body ) ; } ) ; } , err => { teenyRequest . stats . requestFinished ( ) ; callback ( err , null ! , null ) ; } ) ; return ; } teenyRequest . defaults = ( defaults ) => { return ( reqOpts , callback ? ) : Request | void => { const opts = { ... defaults , ... reqOpts } ; if ( callback === undefined ) { return teenyRequest ( opts ) ; } teenyRequest ( opts , callback ) ; } ; } ; /**
 * Single instance of an interface for keeping track of things.
 */ teenyRequest . stats = new TeenyStatistics ( ) ; teenyRequest . resetStats = ( ) : void => { teenyRequest . stats = new TeenyStatistics ( teenyRequest . stats . getOptions ( ) ) ; } ; export { teenyRequest } ;