UNPKG

@deployport/specular-runtime

Version:

Runtime for Specular API clients with support for Node.js and Browser

144 lines (143 loc) 5.5 kB
import { BuiltinMeta } from "../metadata/builtin.js"; import { TypeNotFoundError } from "../metadata/package.js"; import { StructPath } from "../metadata/struct.js"; import { HTTPRequest, StreamMultipartMixedChunks } from "./http.js"; const builtinMeta = BuiltinMeta(); function newHttpErrorException(err) { return new Error(err.message + " " + err.code); } const contentTypeFormat = "+json"; const cleanHTTPContentTypeFormat = (contentType) => { const idx = contentType.indexOf(contentTypeFormat); if (idx === -1) { return contentType; } return contentType.substring(0, idx); }; function multirequireBuildFromJSON(pk, mediaType, json) { try { return builtinMeta.Module.requireBuildFromJSON(mediaType, json); } catch (e) { if (e instanceof TypeNotFoundError) { return pk.requireBuildFromJSON(mediaType, json); } throw e; } } async function parseHTTPResult(pkg, contentType, parseBody) { const outputJSON = await parseBody(); const mediaType = StructPath.fromString(cleanHTTPContentTypeFormat(contentType)); const responseStruct = multirequireBuildFromJSON(pkg, mediaType, outputJSON); if (responseStruct instanceof Error) { throw responseStruct; } return responseStruct; } /** * Returns a copy of original with any overrides applied. If overrides is null, the original is returned. * @param a is the original configuration * @param more is the configuration to add more to the original. * @returns */ export function MergeClientConfig(a, more) { return { ...a, ...more, }; } export default class Client { endpoint; requestConfigurators = []; constructor(config) { if (!config.endpoint) { throw new Error("endpoint is required"); } this.endpoint = config.endpoint; if (config.requestConfigurators) { this.requestConfigurators.push(...config.requestConfigurators); } } async configureRequest(sub) { for (const configurator of this.requestConfigurators) { await configurator(sub); } } async postOperation(operation, inputProps, abortController) { const uri = this.endpoint + "/" + operation.resource.packageUniqueName + "/" + operation.name; const content = await operation.input.serialize(inputProps); const body = JSON.stringify(content); const req = new HTTPRequest(); req.method = "POST"; req.url = uri; req.headers["Content-Type"] = "application/json"; req.body = body; req.abortController = abortController; await this.configureRequest({ operation, request: req, }); const res = await req.fetch(); // if (res.status != 200) { // throw new Error(`failed to stream operation ${res.status} ${res.statusText}`); // } return res; } /** * Executes an operation and returns its single result * @param operation * @param input * @returns the output struct of the operation */ async execute(operation, inputProps) { const res = await this.postOperation(operation, inputProps, new AbortController()); // check if content type is specular/struct const contentType = res.headers.get("Content-Type"); if (!contentType) { throw new Error("invalid response, missing content type"); } const result = await parseHTTPResult(operation.resource.package, contentType, async () => await res.json()); if (builtinMeta.HeartbeatMeta.path === result.__structPath) { throw new Error("unexpected heartbeat"); } else if (builtinMeta.ErrMeta.path === result.__structPath) { throw newHttpErrorException(result); } else if (result instanceof Error) { throw result; } return result; } /** * Execute an operation and stream the results to the provided awaited callback. * The returned promise resolves when the stream is complete. * @param operation * @param input * @param outputCallback returns a promise that resolves when the given output has been processed and the next output can be streamed * @throws Error if the operation fails or the stream fails. The stream will be aborted if the callback throws an error which is the recommended way to stop the stream. */ async stream(operation, input, outputCallback) { const abortController = new AbortController(); const res = await this.postOperation(operation, input, abortController); const partCallback = async (chunk) => { const result = await parseHTTPResult(operation.resource.package, chunk.headers['content-type'] || '', async () => JSON.parse(chunk.body)); if (builtinMeta.HeartbeatMeta.path === result.__structPath) { return; } else if (builtinMeta.ErrMeta.path === result.__structPath) { throw newHttpErrorException(result); } else if (result instanceof Error) { throw result; } await outputCallback(result); }; try { await StreamMultipartMixedChunks(res, partCallback); } finally { // make sure we abort the request when done abortController.abort(); } } }