@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
172 lines (171 loc) • 7.42 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.streamingFetchPromise = void 0;
const SchemaVerifier_1 = require("../SchemaVerifier");
const RequestError_1 = require("../../../errors/RequestError");
const Utils_1 = require("../../Utils");
const StreamParamEncoder_1 = require("./StreamParamEncoder");
const ResponseParamDecoder_1 = require("./ResponseParamDecoder");
const logger = (0, Utils_1.getLogger)("StreamingFetch: ");
//https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#feature_detection
const supportsRequestStreams = (() => {
try {
let duplexAccessed = false;
const request = new Request('https://example.com/', {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
});
const hasContentType = request.headers.has('Content-Type');
return duplexAccessed && !hasContentType;
}
catch (e) {
logger.error("supportsRequestStreams: 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
*/
async function streamingFetchPromise(url, body, schema, timeout, signal, streamRequest) {
if (streamRequest == null)
streamRequest = supportsRequestStreams;
if (timeout != null)
signal = (0, Utils_1.timeoutSignal)(timeout, new Error("Network request timed out"), signal);
const init = {
method: "POST",
headers: {}
};
const startTime = Date.now();
const immediateValues = {};
const promises = [];
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_1.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.duplex = "half";
logger.debug(url + ": Sending request (" + (Date.now() - startTime) + "ms) (streaming): ", immediateValues);
promises.push(outputStream.writeParams(immediateValues));
const abortController = (0, Utils_1.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;
try {
respTxt = await resp.text();
}
catch (e) {
throw new RequestError_1.RequestError(resp.statusText, resp.status);
}
throw new RequestError_1.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 (0, Utils_1.objectMap)(schema, (schemaValue, key) => {
const value = respBody[key];
const result = (0, SchemaVerifier_1.verifyField)(schemaValue, value);
if (result === undefined) {
return Promise.reject(new Error("Invalid field value"));
}
else {
return Promise.resolve(result);
}
});
}
else {
const decoder = new ResponseParamDecoder_1.ResponseParamDecoder(resp, init.signal);
return (0, Utils_1.objectMap)(schema, (schemaValue, key) => decoder.getParam(key).catch(e => {
if ((0, SchemaVerifier_1.isOptionalField)(schemaValue))
return undefined;
throw e;
}).then(value => {
logger.debug(url + ": Response frame read (" + (Date.now() - startTime) + "ms) (streaming resp): ", { [key]: value });
const result = (0, SchemaVerifier_1.verifyField)(schemaValue, value);
if (result === undefined) {
return Promise.reject(new Error("Invalid field value"));
}
else {
return result;
}
}));
}
}
exports.streamingFetchPromise = streamingFetchPromise;