UNPKG

durable-functions

Version:

Durable Functions library for Node.js Azure Functions

650 lines 31.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DurableClient = void 0; const functions_1 = require("@azure/functions"); const OpenTelemetryApi = require("@opentelemetry/api"); const axios_1 = require("axios"); const process = require("process"); const url = require("url"); const validator_1 = require("validator"); const WebhookUtils_1 = require("../util/WebhookUtils"); const DurableOrchestrationStatus_1 = require("../orchestrations/DurableOrchestrationStatus"); const PurgeHistoryResult_1 = require("./PurgeHistoryResult"); const EntityStateResponse_1 = require("../entities/EntityStateResponse"); const OrchestrationRuntimeStatus_1 = require("../orchestrations/OrchestrationRuntimeStatus"); const Utils_1 = require("../util/Utils"); class DurableClient { constructor(clientData) { this.clientData = clientData; this.eventNamePlaceholder = "{eventName}"; this.functionNamePlaceholder = "{functionName}"; this.instanceIdPlaceholder = "[/{instanceId}]"; this.reasonPlaceholder = "{text}"; this.createdTimeFromQueryKey = "createdTimeFrom"; this.createdTimeToQueryKey = "createdTimeTo"; this.runtimeStatusQueryKey = "runtimeStatus"; this.showHistoryQueryKey = "showHistory"; this.showHistoryOutputQueryKey = "showHistoryOutput"; this.showInputQueryKey = "showInput"; this.urlValidationOptions = { protocols: ["http", "https"], require_tld: false, require_protocol: true, require_valid_protocol: true, }; if (!clientData) { throw new TypeError(`clientData: Expected OrchestrationClientInputData but got ${typeof clientData}`); } this.axiosInstance = axios_1.default.create({ validateStatus: (status) => status < 600, headers: { post: { "Content-Type": "application/json", }, }, maxContentLength: Infinity, }); this.taskHubName = this.clientData.taskHubName; this.uniqueWebhookOrigins = this.extractUniqueWebhookOrigins(this.clientData); } createCheckStatusResponse(request, instanceId) { const httpManagementPayload = this.getClientResponseLinks(request, instanceId); return new functions_1.HttpResponse({ status: 202, jsonBody: httpManagementPayload, headers: { "Content-Type": "application/json", Location: httpManagementPayload.statusQueryGetUri, "Retry-After": "10", }, }); } createHttpManagementPayload(instanceId) { return this.getClientResponseLinks(undefined, instanceId); } getStatus(instanceId, options = {}) { return __awaiter(this, void 0, void 0, function* () { const internalOptions = Object.assign({ instanceId }, options); const response = yield this.getStatusInternal(internalOptions); switch (response.status) { case 200: case 202: case 400: if (!response.data) { throw new Error(`DurableClient error: the Durable Functions extension replied with an empty HTTP ${response.status} response.`); } try { return new DurableOrchestrationStatus_1.DurableOrchestrationStatus(response.data); } catch (error) { throw new Error(`DurableClient error: could not construct a DurableOrchestrationStatus object using the data received from the Durable Functions extension: ${error.message}`); } case 404: let msg = `DurableClient error: Durable Functions extension replied with HTTP 404 response. ` + `This usually means we could not find any data associated with the instanceId provided: ${instanceId}.`; if (response.data) { msg += ` Details: ${JSON.stringify(response.data)}`; } throw new Error(msg); case 500: default: return Promise.reject(this.createGenericError(response)); } }); } getStatusAll() { return __awaiter(this, void 0, void 0, function* () { const response = yield this.getStatusInternal({}); switch (response.status) { case 200: return response.data; default: return Promise.reject(this.createGenericError(response)); } }); } getStatusBy(filter) { return __awaiter(this, void 0, void 0, function* () { const response = yield this.getStatusInternal(filter); switch (response.status) { case 200: return response.data; default: return Promise.reject(this.createGenericError(response)); } }); } purgeInstanceHistory(instanceId) { return __awaiter(this, void 0, void 0, function* () { let requestUrl; if (this.clientData.rpcBaseUrl) { requestUrl = new URL(`instances/${instanceId}`, this.clientData.rpcBaseUrl).href; } else { const template = this.clientData.managementUrls.purgeHistoryDeleteUri; const idPlaceholder = this.clientData.managementUrls.id; requestUrl = template.replace(idPlaceholder, instanceId); } const response = yield this.axiosInstance.delete(requestUrl); switch (response.status) { case 200: return response.data; case 404: return new PurgeHistoryResult_1.PurgeHistoryResult(0); default: return Promise.reject(this.createGenericError(response)); } }); } purgeInstanceHistoryBy(filter) { return __awaiter(this, void 0, void 0, function* () { let requestUrl; const { createdTimeFrom, createdTimeTo, runtimeStatus } = filter; if (this.clientData.rpcBaseUrl) { let path = new URL("instances/", this.clientData.rpcBaseUrl).href; const query = []; if (createdTimeFrom) { query.push(`createdTimeFrom=${createdTimeFrom.toISOString()}`); } if (createdTimeTo) { query.push(`createdTimeTo=${createdTimeTo.toISOString()}`); } if (runtimeStatus && runtimeStatus.length > 0) { const statusList = runtimeStatus.map((value) => value.toString()).join(","); query.push(`runtimeStatus=${statusList}`); } if (query.length > 0) { path += "?" + query.join("&"); } requestUrl = new URL(path, this.clientData.rpcBaseUrl).href; } else { const idPlaceholder = this.clientData.managementUrls.id; requestUrl = this.clientData.managementUrls.statusQueryGetUri.replace(idPlaceholder, ""); if (!(createdTimeFrom instanceof Date)) { throw new Error("createdTimeFrom must be a valid Date"); } if (createdTimeFrom) { requestUrl += `&${this.createdTimeFromQueryKey}=${createdTimeFrom.toISOString()}`; } if (createdTimeTo) { requestUrl += `&${this.createdTimeToQueryKey}=${createdTimeTo.toISOString()}`; } if (runtimeStatus && runtimeStatus.length > 0) { const statusesString = runtimeStatus .map((value) => value.toString()) .reduce((acc, curr, i) => { return acc + (i > 0 ? "," : "") + curr; }); requestUrl += `&${this.runtimeStatusQueryKey}=${statusesString}`; } } const response = yield this.axiosInstance.delete(requestUrl); switch (response.status) { case 200: return response.data; case 404: return new PurgeHistoryResult_1.PurgeHistoryResult(0); default: return Promise.reject(this.createGenericError(response)); } }); } raiseEvent(instanceId, eventName, eventData, options = {}) { return __awaiter(this, void 0, void 0, function* () { const { taskHubName, connectionName } = options; if (!eventName) { throw new Error("eventName must be a valid string."); } let requestUrl; if (this.clientData.rpcBaseUrl) { let path = `instances/${instanceId}/raiseEvent/${eventName}`; const query = []; if (taskHubName) { query.push(`taskHub=${taskHubName}`); } if (connectionName) { query.push(`connection=${connectionName}`); } if (query.length > 0) { path += "?" + query.join("&"); } requestUrl = new URL(path, this.clientData.rpcBaseUrl).href; } else { const idPlaceholder = this.clientData.managementUrls.id; requestUrl = this.clientData.managementUrls.sendEventPostUri .replace(idPlaceholder, instanceId) .replace(this.eventNamePlaceholder, eventName); if (taskHubName) { requestUrl = requestUrl.replace(this.clientData.taskHubName, taskHubName); } if (connectionName) { requestUrl = requestUrl.replace(/(connection=)([\w]+)/gi, "$1" + connectionName); } } const response = yield this.axiosInstance.post(requestUrl, JSON.stringify(eventData)); switch (response.status) { case 202: case 410: return; case 404: return Promise.reject(new Error(`No instance with ID '${instanceId}' found.`)); default: return Promise.reject(this.createGenericError(response)); } }); } readEntityState(entityId, options = {}) { return __awaiter(this, void 0, void 0, function* () { let requestUrl; const { taskHubName, connectionName } = options; if (this.clientData.rpcBaseUrl) { let path = `entities/${entityId.name}/${entityId.key}`; const query = []; if (taskHubName) { query.push(`taskHub=${taskHubName}`); } if (connectionName) { query.push(`connection=${connectionName}`); } if (query.length > 0) { path += "?" + query.join("&"); } requestUrl = new URL(path, this.clientData.rpcBaseUrl).href; } else { if (!(this.clientData.baseUrl && this.clientData.requiredQueryStringParameters)) { throw new Error("Cannot use the readEntityState API with this version of the Durable Task Extension."); } requestUrl = WebhookUtils_1.WebhookUtils.getReadEntityUrl(this.clientData.baseUrl, this.clientData.requiredQueryStringParameters, entityId.name, entityId.key, taskHubName, connectionName); } const response = yield this.axiosInstance.get(requestUrl); switch (response.status) { case 200: return new EntityStateResponse_1.EntityStateResponse(true, response.data); case 404: return new EntityStateResponse_1.EntityStateResponse(false); default: return Promise.reject(this.createGenericError(response)); } }); } rewind(instanceId, reason, options = {}) { return __awaiter(this, void 0, void 0, function* () { const { taskHubName, connectionName } = options; const idPlaceholder = this.clientData.managementUrls.id; let requestUrl; if (this.clientData.rpcBaseUrl) { let path = `instances/${instanceId}/rewind?reason=${reason}`; const query = []; if (taskHubName) { query.push(`taskHub=${taskHubName}`); } if (connectionName) { query.push(`connection=${connectionName}`); } if (query.length > 0) { path += "&" + query.join("&"); } requestUrl = new URL(path, this.clientData.rpcBaseUrl).href; } else { requestUrl = this.clientData.managementUrls.rewindPostUri .replace(idPlaceholder, instanceId) .replace(this.reasonPlaceholder, reason); } const response = yield this.axiosInstance.post(requestUrl); switch (response.status) { case 202: return; case 404: return Promise.reject(new Error(`No instance with ID '${instanceId}' found.`)); case 410: return Promise.reject(new Error("The rewind operation is only supported on failed orchestration instances.")); default: return Promise.reject(this.createGenericError(response)); } }); } signalEntity(entityId, operationName, operationContent, options = {}) { return __awaiter(this, void 0, void 0, function* () { const { taskHubName, connectionName } = options; let requestUrl; if (this.clientData.rpcBaseUrl) { let path = `entities/${entityId.name}/${entityId.key}`; const query = []; if (operationName) { query.push(`op=${operationName}`); } if (taskHubName) { query.push(`taskHub=${taskHubName}`); } if (connectionName) { query.push(`connection=${connectionName}`); } if (query.length > 0) { path += "?" + query.join("&"); } requestUrl = new URL(path, this.clientData.rpcBaseUrl).href; } else { if (!(this.clientData.baseUrl && this.clientData.requiredQueryStringParameters)) { throw new Error("Cannot use the signalEntity API with this version of the Durable Task Extension."); } requestUrl = WebhookUtils_1.WebhookUtils.getSignalEntityUrl(this.clientData.baseUrl, this.clientData.requiredQueryStringParameters, entityId.name, entityId.key, operationName, taskHubName, connectionName); } const headers = this.getDistributedTracingHeaders(); const response = yield this.axiosInstance.post(requestUrl, JSON.stringify(operationContent), { headers }); switch (response.status) { case 202: return; default: return Promise.reject(this.createGenericError(response)); } }); } startNew(orchestratorFunctionName, options) { return __awaiter(this, void 0, void 0, function* () { if (!orchestratorFunctionName) { throw new Error("orchestratorFunctionName must be a valid string."); } let requestUrl; const instanceIdPath = (options === null || options === void 0 ? void 0 : options.instanceId) ? `/${options.instanceId}` : ""; if (this.clientData.rpcBaseUrl) { requestUrl = new URL(`orchestrators/${orchestratorFunctionName}${instanceIdPath}`, this.clientData.rpcBaseUrl).href; } else { requestUrl = this.clientData.creationUrls.createNewInstancePostUri; requestUrl = requestUrl .replace(this.functionNamePlaceholder, orchestratorFunctionName) .replace(this.instanceIdPlaceholder, instanceIdPath); } const headers = this.getDistributedTracingHeaders(); const input = (options === null || options === void 0 ? void 0 : options.input) !== undefined ? JSON.stringify(options.input) : ""; const response = yield this.axiosInstance.post(requestUrl, input, { headers }); if (response.data && response.status <= 202) { return response.data.id; } else { return Promise.reject(this.createGenericError(response)); } }); } terminate(instanceId, reason) { return __awaiter(this, void 0, void 0, function* () { const idPlaceholder = this.clientData.managementUrls.id; let requestUrl; if (this.clientData.rpcBaseUrl) { requestUrl = new URL(`instances/${instanceId}/terminate?reason=${reason}`, this.clientData.rpcBaseUrl).href; } else { requestUrl = this.clientData.managementUrls.terminatePostUri .replace(idPlaceholder, instanceId) .replace(this.reasonPlaceholder, reason); } const response = yield this.axiosInstance.post(requestUrl); switch (response.status) { case 202: case 410: return; case 404: return Promise.reject(new Error(`No instance with ID '${instanceId}' found.`)); default: return Promise.reject(this.createGenericError(response)); } }); } suspend(instanceId, reason) { return __awaiter(this, void 0, void 0, function* () { const idPlaceholder = this.clientData.managementUrls.id; let requestUrl; if (this.clientData.rpcBaseUrl) { requestUrl = new URL(`instances/${instanceId}/suspend?reason=${reason}`, this.clientData.rpcBaseUrl).href; } else { requestUrl = this.clientData.managementUrls.suspendPostUri .replace(idPlaceholder, instanceId) .replace(this.reasonPlaceholder, reason); } const response = yield this.axiosInstance.post(requestUrl); switch (response.status) { case 202: case 410: return; case 404: return Promise.reject(new Error(`No instance with ID '${instanceId}' found.`)); default: return Promise.reject(this.createGenericError(response)); } }); } resume(instanceId, reason) { return __awaiter(this, void 0, void 0, function* () { const idPlaceholder = this.clientData.managementUrls.id; let requestUrl; if (this.clientData.rpcBaseUrl) { requestUrl = new URL(`instances/${instanceId}/resume?reason=${reason}`, this.clientData.rpcBaseUrl).href; } else { requestUrl = this.clientData.managementUrls.resumePostUri .replace(idPlaceholder, instanceId) .replace(this.reasonPlaceholder, reason); } const response = yield this.axiosInstance.post(requestUrl); switch (response.status) { case 202: case 410: return; case 404: return Promise.reject(new Error(`No instance with ID '${instanceId}' found.`)); default: return Promise.reject(this.createGenericError(response)); } }); } waitForCompletionOrCreateCheckStatusResponse(request, instanceId, waitOptions = {}) { return __awaiter(this, void 0, void 0, function* () { const timeoutInMilliseconds = waitOptions.timeoutInMilliseconds !== undefined ? waitOptions.timeoutInMilliseconds : 10000; const retryIntervalInMilliseconds = waitOptions.retryIntervalInMilliseconds !== undefined ? waitOptions.retryIntervalInMilliseconds : 1000; if (retryIntervalInMilliseconds > timeoutInMilliseconds) { throw new Error(`Total timeout ${timeoutInMilliseconds} (ms) should be bigger than retry timeout ${retryIntervalInMilliseconds} (ms)`); } const hrStart = process.hrtime(); while (true) { const status = yield this.getStatus(instanceId); if (status) { switch (status.runtimeStatus) { case OrchestrationRuntimeStatus_1.OrchestrationRuntimeStatus.Completed: return this.createHttpResponse(200, status.output); case OrchestrationRuntimeStatus_1.OrchestrationRuntimeStatus.Canceled: case OrchestrationRuntimeStatus_1.OrchestrationRuntimeStatus.Terminated: return this.createHttpResponse(200, status); case OrchestrationRuntimeStatus_1.OrchestrationRuntimeStatus.Failed: return this.createHttpResponse(500, status); } } const hrElapsed = process.hrtime(hrStart); const hrElapsedMilliseconds = Utils_1.Utils.getHrMilliseconds(hrElapsed); if (hrElapsedMilliseconds < timeoutInMilliseconds) { const remainingTime = timeoutInMilliseconds - hrElapsedMilliseconds; yield Utils_1.Utils.sleep(remainingTime > retryIntervalInMilliseconds ? retryIntervalInMilliseconds : remainingTime); } else { return this.createCheckStatusResponse(request, instanceId); } } }); } getDistributedTracingHeaders() { const currentSpan = OpenTelemetryApi.trace.getSpan(OpenTelemetryApi.context.active()); const headers = {}; if (currentSpan) { OpenTelemetryApi.propagation.inject(OpenTelemetryApi.context.active(), headers); } return headers; } createHttpResponse(statusCode, body) { return new functions_1.HttpResponse({ status: statusCode, jsonBody: body, headers: { "Content-Type": "application/json", }, }); } getClientResponseLinks(request, instanceId) { const payload = Object.assign({}, this.clientData.managementUrls); Object.keys(payload).forEach((key) => { if (this.hasValidRequestUrl(request) && (0, validator_1.isURL)(payload[key], this.urlValidationOptions)) { const requestUrl = new url.URL(request.url); const dataUrl = new url.URL(payload[key]); payload[key] = payload[key].replace(dataUrl.origin, requestUrl.origin); } payload[key] = payload[key].replace(this.clientData.managementUrls.id, instanceId); }); return payload; } hasValidRequestUrl(request) { return request !== undefined && request.url !== undefined; } extractUniqueWebhookOrigins(clientData) { const origins = this.extractWebhookOrigins(clientData.creationUrls).concat(this.extractWebhookOrigins(clientData.managementUrls)); const uniqueOrigins = origins.reduce((acc, curr) => { if (acc.indexOf(curr) === -1) { acc.push(curr); } return acc; }, []); return uniqueOrigins; } extractWebhookOrigins(obj) { const origins = []; const keys = Object.getOwnPropertyNames(obj); keys.forEach((key) => { const value = obj[key]; if ((0, validator_1.isURL)(value, this.urlValidationOptions)) { const valueAsUrl = new url.URL(value); const origin = valueAsUrl.origin; origins.push(origin); } }); return origins; } getStatusInternal(options, continuationToken, prevData) { return __awaiter(this, void 0, void 0, function* () { let requestUrl; if (this.clientData.rpcBaseUrl) { let path = new URL(`instances/${options.instanceId || ""}`, this.clientData.rpcBaseUrl) .href; const query = []; if (options.taskHubName) { query.push(`taskHub=${options.taskHubName}`); } if (options.connectionName) { query.push(`connection=${options.connectionName}`); } if (options.showHistory) { query.push(`showHistory=${options.showHistory}`); } if (options.showHistoryOutput) { query.push(`showHistoryOutput=${options.showHistoryOutput}`); } if (options.showInput) { query.push(`showInput=${options.showInput}`); } if (options.createdTimeFrom) { query.push(`createdTimeFrom=${options.createdTimeFrom.toISOString()}`); } if (options.createdTimeTo) { query.push(`createdTimeTo=${options.createdTimeTo.toISOString()}`); } if (options.runtimeStatus && options.runtimeStatus.length > 0) { const statusList = options.runtimeStatus .map((value) => value.toString()) .join(","); query.push(`runtimeStatus=${statusList}`); } if (query.length > 0) { path += "?" + query.join("&"); } requestUrl = new URL(path, this.clientData.rpcBaseUrl).href; } else { const template = this.clientData.managementUrls.statusQueryGetUri; const idPlaceholder = this.clientData.managementUrls.id; requestUrl = template.replace(idPlaceholder, typeof options.instanceId === "string" ? options.instanceId : ""); if (options.taskHubName) { requestUrl = requestUrl.replace(this.clientData.taskHubName, options.taskHubName); } if (options.connectionName) { requestUrl = requestUrl.replace(/(connection=)([\w]+)/gi, "$1" + options.connectionName); } if (options.showHistory) { requestUrl += `&${this.showHistoryQueryKey}=${options.showHistory}`; } if (options.showHistoryOutput) { requestUrl += `&${this.showHistoryOutputQueryKey}=${options.showHistoryOutput}`; } if (options.createdTimeFrom) { requestUrl += `&${this.createdTimeFromQueryKey}=${options.createdTimeFrom.toISOString()}`; } if (options.createdTimeTo) { requestUrl += `&${this.createdTimeToQueryKey}=${options.createdTimeTo.toISOString()}`; } if (options.runtimeStatus && options.runtimeStatus.length > 0) { const statusesString = options.runtimeStatus .map((value) => value.toString()) .reduce((acc, curr, i) => { return acc + (i > 0 ? "," : "") + curr; }); requestUrl += `&${this.runtimeStatusQueryKey}=${statusesString}`; } if (typeof options.showInput === "boolean") { requestUrl += `&${this.showInputQueryKey}=${options.showInput}`; } } let axiosConfig = undefined; if (continuationToken) { axiosConfig = { headers: { "x-ms-continuation-token": continuationToken, }, }; } const response = this.axiosInstance.get(requestUrl, axiosConfig).then((httpResponse) => { const headers = httpResponse.headers; if (prevData) { httpResponse.data = prevData.concat(httpResponse.data); } const token = headers["x-ms-continuation-token"]; if (token) { return this.getStatusInternal(options, token, httpResponse.data); } return httpResponse; }); return response; }); } createGenericError(response) { return new Error(`The operation failed with an unexpected status code: ${response.status}. Details: ${JSON.stringify(response.data)}`); } } exports.DurableClient = DurableClient; //# sourceMappingURL=DurableClient.js.map