durable-functions
Version:
Durable Functions library for Node.js Azure Functions
650 lines • 31.4 kB
JavaScript
"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