@deployport/specular-runtime
Version:
Runtime for Specular API clients with support for Node.js and Browser
144 lines (143 loc) • 5.5 kB
JavaScript
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();
}
}
}