UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

190 lines (160 loc) 7.19 kB
import {isOptionalField, RequestSchema, RequestSchemaResultPromise, verifyField} from "../SchemaVerifier"; import {RequestError} from "../../../errors/RequestError"; import {extendAbortController, getLogger, objectMap, timeoutSignal} from "../../Utils"; import {StreamParamEncoder} from "./StreamParamEncoder"; import {ResponseParamDecoder} from "./ResponseParamDecoder"; export type RequestBody = { [key: string]: Promise<any> | any } const logger = getLogger("StreamingFetch: "); //https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#feature_detection const supportsRequestStreams: boolean = (() => { try { let duplexAccessed = false; const request = new Request('https://example.com/', { body: new ReadableStream(), method: 'POST', get duplex() { duplexAccessed = true; return 'half'; }, } as any); const hasContentType = request.headers.has('Content-Type'); return duplexAccessed && !hasContentType; } catch (e) { console.error("Error checking environment support for HTTP request stream", e); return false; } })(); logger.info("Environment supports request stream: "+supportsRequestStreams); /** * Sends a POST request to the specified URL in a streaming request/response mode * * @param url URL to send the request to * @param body An object containing properties that should be sent to the server, can be Promise or any * @param schema Schema of the response that should be received from the server * @param timeout Timeout in millseconds for the request to succeed & all its response properties to resolve * @param signal Abort signal * @param streamRequest Whether the request should be streamed or not * @throws {RequestError} When the response code is not 200 */ export async function streamingFetchPromise<T extends RequestSchema>( url: string, body: RequestBody, schema: T, timeout?: number, signal?: AbortSignal, streamRequest?: boolean ): Promise<RequestSchemaResultPromise<T>> { if(streamRequest==null) streamRequest = supportsRequestStreams; if(timeout!=null) signal = timeoutSignal(timeout, new Error("Network request timed out"), signal); const init: RequestInit = { method: "POST", headers: {} }; const startTime = Date.now(); const immediateValues: any = {}; const promises: Promise<any>[] = []; if(!streamRequest) { for(let key in body) { if(body[key] instanceof Promise) { promises.push(body[key].then((val) => { immediateValues[key] = val; })); } else { immediateValues[key] = body[key]; } } try { await Promise.all(promises); } catch (e) { e._inputPromiseError = true; throw e; } if(signal!=null) signal.throwIfAborted(); logger.debug(url+": Sending request ("+(Date.now()-startTime)+"ms) (non-streaming): ", immediateValues); init.body = JSON.stringify(immediateValues); init.headers['content-type'] = "application/json"; } else { const outputStream = new StreamParamEncoder(); let hasPromiseInBody = false; for(let key in body) { if(body[key] instanceof Promise) { promises.push(body[key].then((val) => { logger.debug(url+": Send param ("+(Date.now()-startTime)+"ms) (streaming): ", {[key]: val}); return outputStream.writeParams({ [key]: val }); })); hasPromiseInBody = true; } else { immediateValues[key] = body[key]; } } if(hasPromiseInBody) { init.body = outputStream.getReadableStream(); init.headers['content-type'] = "application/x-multiple-json"; (init as any).duplex = "half"; logger.debug(url+": Sending request ("+(Date.now()-startTime)+"ms) (streaming): ", immediateValues); promises.push(outputStream.writeParams(immediateValues)); const abortController = extendAbortController(signal); signal = abortController.signal; Promise.all(promises).then(() => outputStream.end()).catch(e => { e._inputPromiseError = true; abortController.abort(e); }); signal.addEventListener("abort", () => outputStream.end()); } else { logger.debug(url+": Sending request ("+(Date.now()-startTime)+"ms) (non-streaming): ", immediateValues); init.body = JSON.stringify(immediateValues); init.headers['content-type'] = "application/json"; } } if(signal!=null) init.signal = signal; init.headers['accept'] = "application/x-multiple-json"; const resp = await fetch(url, init).catch(e => { if(init.signal!=null && e.name==="AbortError") { throw init.signal.reason; } else { if(e.message!=null) e.message += streamRequest ? " (streaming req)" : " (non streaming req)" throw e; } }); logger.debug(url+": Response status ("+(Date.now()-startTime)+"ms) "+(streamRequest ? "(streaming req)" : "(non streaming req)")+": ", resp.status); if(resp.status!==200) { let respTxt: string; try { respTxt = await resp.text(); } catch (e) { throw new RequestError(resp.statusText, resp.status); } throw new RequestError(respTxt, resp.status); } if(resp.headers.get("content-type")!=="application/x-multiple-json") { const respBody = await resp.json(); logger.debug(url+": Response read ("+(Date.now()-startTime)+"ms) (non streaming resp): ", respBody); return objectMap(schema, (schemaValue, key) => { const value = respBody[key]; const result = verifyField(schemaValue, value); if(result===undefined) { return Promise.reject(new Error("Invalid field value")); } else { return Promise.resolve(result); } }) as any; } else { const decoder = new ResponseParamDecoder(resp, init.signal); return objectMap(schema, (schemaValue, key) => decoder.getParam(key).catch(e => { if(isOptionalField(schemaValue)) return undefined; throw e; }).then(value => { logger.debug(url+": Response frame read ("+(Date.now()-startTime)+"ms) (streaming resp): ", {[key]: value}); const result = verifyField(schemaValue, value); if(result===undefined) { return Promise.reject(new Error("Invalid field value")); } else { return result; } })) as any; } }