@temporalio/client
Version:
Temporal.io SDK Client sub-package
990 lines • 51.8 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryNotRegisteredError = exports.QueryRejectedError = exports.WorkflowClient = exports.WithStartWorkflowOperation = void 0;
const grpc_js_1 = require("@grpc/grpc-js");
const uuid_1 = require("uuid");
const common_1 = require("@temporalio/common");
const codec_helpers_1 = require("@temporalio/common/lib/internal-non-workflow/codec-helpers");
const payload_search_attributes_1 = require("@temporalio/common/lib/converter/payload-search-attributes");
const interceptors_1 = require("@temporalio/common/lib/interceptors");
const type_helpers_1 = require("@temporalio/common/lib/type-helpers");
const internal_non_workflow_1 = require("@temporalio/common/lib/internal-non-workflow");
const internal_workflow_1 = require("@temporalio/common/lib/internal-workflow");
const proto_1 = require("@temporalio/proto");
const errors_1 = require("./errors");
const types_1 = require("./types");
const workflow_options_1 = require("./workflow-options");
const helpers_1 = require("./helpers");
const base_client_1 = require("./base-client");
const iterators_utils_1 = require("./iterators-utils");
const workflow_update_stage_1 = require("./workflow-update-stage");
const internal_1 = require("./internal");
const UpdateWorkflowExecutionLifecycleStage = proto_1.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage;
function defaultWorkflowClientOptions() {
return {
...(0, base_client_1.defaultBaseClientOptions)(),
interceptors: [],
queryRejectCondition: 'NONE',
};
}
function assertRequiredWorkflowOptions(opts) {
if (!opts.taskQueue) {
throw new TypeError('Missing WorkflowOptions.taskQueue');
}
if (!opts.workflowId) {
throw new TypeError('Missing WorkflowOptions.workflowId');
}
}
function ensureArgs(opts) {
const { args, ...rest } = opts;
return { args: args ?? [], ...rest };
}
const withStartWorkflowOperationResolve = Symbol();
const withStartWorkflowOperationReject = Symbol();
const withStartWorkflowOperationUsed = Symbol();
/**
* Define how to start a workflow when using {@link WorkflowClient.startUpdateWithStart} and
* {@link WorkflowClient.executeUpdateWithStart}. `workflowIdConflictPolicy` is required in the options.
*/
class WithStartWorkflowOperation {
workflowTypeOrFunc;
options;
[withStartWorkflowOperationUsed] = false;
[withStartWorkflowOperationResolve] = undefined;
[withStartWorkflowOperationReject] = undefined;
workflowHandlePromise;
constructor(workflowTypeOrFunc, options) {
this.workflowTypeOrFunc = workflowTypeOrFunc;
this.options = options;
this.workflowHandlePromise = new Promise((resolve, reject) => {
this[withStartWorkflowOperationResolve] = resolve;
this[withStartWorkflowOperationReject] = reject;
});
}
async workflowHandle() {
return await this.workflowHandlePromise;
}
}
exports.WithStartWorkflowOperation = WithStartWorkflowOperation;
/**
* Client for starting Workflow executions and creating Workflow handles.
*
* Typically this client should not be instantiated directly, instead create the high level {@link Client} and use
* {@link Client.workflow} to interact with Workflows.
*/
class WorkflowClient extends base_client_1.BaseClient {
options;
constructor(options) {
super(options);
this.options = {
...defaultWorkflowClientOptions(),
...(0, internal_workflow_1.filterNullAndUndefined)(options ?? {}),
loadedDataConverter: this.dataConverter,
};
}
/**
* Raw gRPC access to the Temporal service.
*
* **NOTE**: The namespace provided in {@link options} is **not** automatically set on requests made via this service
* object.
*/
get workflowService() {
return this.connection.workflowService;
}
async _start(workflowTypeOrFunc, options, interceptors) {
const workflowType = (0, common_1.extractWorkflowType)(workflowTypeOrFunc);
assertRequiredWorkflowOptions(options);
const compiledOptions = (0, workflow_options_1.compileWorkflowOptions)(ensureArgs(options));
const start = (0, interceptors_1.composeInterceptors)(interceptors, 'start', this._startWorkflowHandler.bind(this));
return start({
options: compiledOptions,
headers: {},
workflowType,
});
}
async _signalWithStart(workflowTypeOrFunc, options, interceptors) {
const workflowType = (0, common_1.extractWorkflowType)(workflowTypeOrFunc);
const { signal, signalArgs, ...rest } = options;
assertRequiredWorkflowOptions(rest);
const compiledOptions = (0, workflow_options_1.compileWorkflowOptions)(ensureArgs(rest));
const signalWithStart = (0, interceptors_1.composeInterceptors)(interceptors, 'signalWithStart', this._signalWithStartWorkflowHandler.bind(this));
return signalWithStart({
options: compiledOptions,
headers: {},
workflowType,
signalName: typeof signal === 'string' ? signal : signal.name,
signalArgs: signalArgs ?? [],
});
}
/**
* Start a new Workflow execution.
*
* @returns a {@link WorkflowHandle} to the started Workflow
*/
async start(workflowTypeOrFunc, options) {
const { workflowId } = options;
const interceptors = this.getOrMakeInterceptors(workflowId);
const runId = await this._start(workflowTypeOrFunc, { ...options, workflowId }, interceptors);
// runId is not used in handles created with `start*` calls because these
// handles should allow interacting with the workflow if it continues as new.
const handle = this._createWorkflowHandle({
workflowId,
runId: undefined,
firstExecutionRunId: runId,
runIdForResult: runId,
interceptors,
followRuns: options.followRuns ?? true,
}); // Cast is safe because we know we add the firstExecutionRunId below
handle /* readonly */.firstExecutionRunId = runId;
return handle;
}
/**
* Start a new Workflow Execution and immediately send a Signal to that Workflow.
*
* The behavior of Signal-with-Start in the case where there is already a running Workflow with
* the given Workflow ID depends on the {@link WorkflowIDConflictPolicy}. That is, if the policy
* is `USE_EXISTING`, then the Signal is issued against the already existing Workflow Execution;
* however, if the policy is `FAIL`, then an error is thrown. If no policy is specified,
* Signal-with-Start defaults to `USE_EXISTING`.
*
* @returns a {@link WorkflowHandle} to the started Workflow
*/
async signalWithStart(workflowTypeOrFunc, options) {
const { workflowId } = options;
const interceptors = this.getOrMakeInterceptors(workflowId);
const runId = await this._signalWithStart(workflowTypeOrFunc, options, interceptors);
// runId is not used in handles created with `start*` calls because these
// handles should allow interacting with the workflow if it continues as new.
const handle = this._createWorkflowHandle({
workflowId,
runId: undefined,
firstExecutionRunId: undefined, // We don't know if this runId is first in the chain or not
runIdForResult: runId,
interceptors,
followRuns: options.followRuns ?? true,
}); // Cast is safe because we know we add the signaledRunId below
handle /* readonly */.signaledRunId = runId;
return handle;
}
/**
* Start a new Workflow Execution and immediately send an Update to that Workflow,
* then await and return the Update's result.
*
* The `updateOptions` object must contain a {@link WithStartWorkflowOperation}, which defines
* the options for the Workflow execution to start (e.g. the Workflow's type, task queue, input
* arguments, etc.)
*
* The behavior of Update-with-Start in the case where there is already a running Workflow with
* the given Workflow ID depends on the specified {@link WorkflowIDConflictPolicy}. That is, if
* the policy is `USE_EXISTING`, then the Update is issued against the already existing Workflow
* Execution; however, if the policy is `FAIL`, then an error is thrown. Caller MUST specify
* the desired WorkflowIDConflictPolicy.
*
* This call will block until the Update has completed. The Workflow handle can be retrieved by
* awaiting on {@link WithStartWorkflowOperation.workflowHandle}, whether or not the Update
* succeeds.
*
* @returns the Update result
*/
async executeUpdateWithStart(updateDef, updateOptions) {
const handle = await this._startUpdateWithStart(updateDef, {
...updateOptions,
waitForStage: workflow_update_stage_1.WorkflowUpdateStage.COMPLETED,
});
return await handle.result();
}
/**
* Start a new Workflow Execution and immediately send an Update to that Workflow,
* then return a {@link WorkflowUpdateHandle} for that Update.
*
* The `updateOptions` object must contain a {@link WithStartWorkflowOperation}, which defines
* the options for the Workflow execution to start (e.g. the Workflow's type, task queue, input
* arguments, etc.)
*
* The behavior of Update-with-Start in the case where there is already a running Workflow with
* the given Workflow ID depends on the specified {@link WorkflowIDConflictPolicy}. That is, if
* the policy is `USE_EXISTING`, then the Update is issued against the already existing Workflow
* Execution; however, if the policy is `FAIL`, then an error is thrown. Caller MUST specify
* the desired WorkflowIDConflictPolicy.
*
* This call will block until the Update has reached the specified {@link WorkflowUpdateStage}.
* Note that this means that the call will not return successfully until the Update has
* been delivered to a Worker. The Workflow handle can be retrieved by awaiting on
* {@link WithStartWorkflowOperation.workflowHandle}, whether or not the Update succeeds.
*
* @returns a {@link WorkflowUpdateHandle} to the started Update
*/
async startUpdateWithStart(updateDef, updateOptions) {
return this._startUpdateWithStart(updateDef, updateOptions);
}
async _startUpdateWithStart(updateDef, updateWithStartOptions) {
const { waitForStage, args, startWorkflowOperation, ...updateOptions } = updateWithStartOptions;
const { workflowTypeOrFunc, options: workflowOptions } = startWorkflowOperation;
const { workflowId } = workflowOptions;
if (startWorkflowOperation[withStartWorkflowOperationUsed]) {
throw new Error('This WithStartWorkflowOperation instance has already been executed.');
}
startWorkflowOperation[withStartWorkflowOperationUsed] = true;
assertRequiredWorkflowOptions(workflowOptions);
const startUpdateWithStartInput = {
workflowType: (0, common_1.extractWorkflowType)(workflowTypeOrFunc),
workflowStartOptions: (0, workflow_options_1.compileWorkflowOptions)(ensureArgs(workflowOptions)),
workflowStartHeaders: {},
updateName: typeof updateDef === 'string' ? updateDef : updateDef.name,
updateArgs: args ?? [],
updateOptions,
updateHeaders: {},
};
const interceptors = this.getOrMakeInterceptors(workflowId);
const onStart = (startResponse) => startWorkflowOperation[withStartWorkflowOperationResolve](this._createWorkflowHandle({
workflowId,
firstExecutionRunId: startResponse.runId ?? undefined,
interceptors,
followRuns: workflowOptions.followRuns ?? true,
}));
const onStartError = (err) => {
startWorkflowOperation[withStartWorkflowOperationReject](err);
};
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'startUpdateWithStart', this._updateWithStartHandler.bind(this, waitForStage, onStart, onStartError));
const updateOutput = await fn(startUpdateWithStartInput);
let outcome = updateOutput.updateOutcome;
if (!outcome && waitForStage === workflow_update_stage_1.WorkflowUpdateStage.COMPLETED) {
outcome = await this._pollForUpdateOutcome(updateOutput.updateId, {
workflowId,
runId: updateOutput.workflowExecution.runId,
});
}
return this.createWorkflowUpdateHandle(updateOutput.updateId, workflowId, updateOutput.workflowExecution.runId, outcome);
}
/**
* Start a new Workflow execution, then await for its completion and return that Workflow's result.
*
* @returns the result of the Workflow execution
*/
async execute(workflowTypeOrFunc, options) {
const { workflowId } = options;
const interceptors = this.getOrMakeInterceptors(workflowId);
await this._start(workflowTypeOrFunc, options, interceptors);
return await this.result(workflowId, undefined, {
...options,
followRuns: options.followRuns ?? true,
});
}
/**
* Get the result of a Workflow execution.
*
* Follow the chain of execution in case Workflow continues as new, or has a cron schedule or retry policy.
*/
async result(workflowId, runId, opts) {
const followRuns = opts?.followRuns ?? true;
const execution = { workflowId, runId };
const req = {
namespace: this.options.namespace,
execution,
skipArchival: true,
waitNewEvent: true,
historyEventFilterType: proto_1.temporal.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT,
};
let ev;
for (;;) {
let res;
try {
res = await this.workflowService.getWorkflowExecutionHistory(req);
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to get Workflow execution history', { workflowId, runId });
}
const events = res.history?.events;
if (events == null || events.length === 0) {
req.nextPageToken = res.nextPageToken;
continue;
}
if (events.length !== 1) {
throw new Error(`Expected at most 1 close event(s), got: ${events.length}`);
}
ev = events[0];
if (ev.workflowExecutionCompletedEventAttributes) {
if (followRuns && ev.workflowExecutionCompletedEventAttributes.newExecutionRunId) {
execution.runId = ev.workflowExecutionCompletedEventAttributes.newExecutionRunId;
req.nextPageToken = undefined;
continue;
}
// Note that we can only return one value from our workflow function in JS.
// Ignore any other payloads in result
const [result] = await (0, internal_non_workflow_1.decodeArrayFromPayloads)(this.dataConverter, ev.workflowExecutionCompletedEventAttributes.result?.payloads);
return result;
}
else if (ev.workflowExecutionFailedEventAttributes) {
if (followRuns && ev.workflowExecutionFailedEventAttributes.newExecutionRunId) {
execution.runId = ev.workflowExecutionFailedEventAttributes.newExecutionRunId;
req.nextPageToken = undefined;
continue;
}
const { failure, retryState } = ev.workflowExecutionFailedEventAttributes;
throw new errors_1.WorkflowFailedError('Workflow execution failed', await (0, internal_non_workflow_1.decodeOptionalFailureToOptionalError)(this.dataConverter, failure), (0, common_1.decodeRetryState)(retryState));
}
else if (ev.workflowExecutionCanceledEventAttributes) {
const failure = new common_1.CancelledFailure('Workflow canceled', await (0, internal_non_workflow_1.decodeArrayFromPayloads)(this.dataConverter, ev.workflowExecutionCanceledEventAttributes.details?.payloads));
failure.stack = '';
throw new errors_1.WorkflowFailedError('Workflow execution cancelled', failure, common_1.RetryState.NON_RETRYABLE_FAILURE);
}
else if (ev.workflowExecutionTerminatedEventAttributes) {
const failure = new common_1.TerminatedFailure(ev.workflowExecutionTerminatedEventAttributes.reason || 'Workflow execution terminated');
failure.stack = '';
throw new errors_1.WorkflowFailedError(ev.workflowExecutionTerminatedEventAttributes.reason || 'Workflow execution terminated', failure, common_1.RetryState.NON_RETRYABLE_FAILURE);
}
else if (ev.workflowExecutionTimedOutEventAttributes) {
if (followRuns && ev.workflowExecutionTimedOutEventAttributes.newExecutionRunId) {
execution.runId = ev.workflowExecutionTimedOutEventAttributes.newExecutionRunId;
req.nextPageToken = undefined;
continue;
}
const failure = new common_1.TimeoutFailure('Workflow execution timed out', undefined, common_1.TimeoutType.START_TO_CLOSE);
failure.stack = '';
throw new errors_1.WorkflowFailedError('Workflow execution timed out', failure, (0, common_1.decodeRetryState)(ev.workflowExecutionTimedOutEventAttributes.retryState));
}
else if (ev.workflowExecutionContinuedAsNewEventAttributes) {
const { newExecutionRunId } = ev.workflowExecutionContinuedAsNewEventAttributes;
if (!newExecutionRunId) {
throw new TypeError('Expected service to return newExecutionRunId for WorkflowExecutionContinuedAsNewEvent');
}
if (!followRuns) {
throw new errors_1.WorkflowContinuedAsNewError('Workflow execution continued as new', newExecutionRunId);
}
execution.runId = newExecutionRunId;
req.nextPageToken = undefined;
continue;
}
}
}
rethrowUpdateGrpcError(err, fallbackMessage, workflowExecution) {
if ((0, errors_1.isGrpcServiceError)(err)) {
if (err.code === grpc_js_1.status.DEADLINE_EXCEEDED || err.code === grpc_js_1.status.CANCELLED) {
throw new errors_1.WorkflowUpdateRPCTimeoutOrCancelledError(err.details ?? 'Workflow update call timeout or cancelled', {
cause: err,
});
}
}
if (err instanceof common_1.CancelledFailure) {
throw new errors_1.WorkflowUpdateRPCTimeoutOrCancelledError(err.message ?? 'Workflow update call timeout or cancelled', {
cause: err,
});
}
this.rethrowGrpcError(err, fallbackMessage, workflowExecution);
}
rethrowGrpcError(err, fallbackMessage, workflowExecution) {
if ((0, errors_1.isGrpcServiceError)(err)) {
(0, helpers_1.rethrowKnownErrorTypes)(err);
if (err.code === grpc_js_1.status.NOT_FOUND) {
throw new common_1.WorkflowNotFoundError(err.details ?? 'Workflow not found', workflowExecution?.workflowId ?? '', workflowExecution?.runId);
}
throw new errors_1.ServiceError(fallbackMessage, { cause: err });
}
throw new errors_1.ServiceError('Unexpected error while making gRPC request', { cause: err });
}
/**
* Use given input to make a queryWorkflow call to the service
*
* Used as the final function of the query interceptor chain
*/
async _queryWorkflowHandler(input) {
const req = {
queryRejectCondition: input.queryRejectCondition,
namespace: this.options.namespace,
execution: input.workflowExecution,
query: {
queryType: input.queryType,
queryArgs: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...input.args) },
header: { fields: input.headers },
},
};
let response;
try {
response = await this.workflowService.queryWorkflow(req);
}
catch (err) {
if ((0, errors_1.isGrpcServiceError)(err)) {
(0, helpers_1.rethrowKnownErrorTypes)(err);
if (err.code === grpc_js_1.status.INVALID_ARGUMENT) {
throw new QueryNotRegisteredError(err.message.replace(/^3 INVALID_ARGUMENT: /, ''), err.code);
}
}
this.rethrowGrpcError(err, 'Failed to query Workflow', input.workflowExecution);
}
if (response.queryRejected) {
if (response.queryRejected.status === undefined || response.queryRejected.status === null) {
throw new TypeError('Received queryRejected from server with no status');
}
throw new QueryRejectedError(response.queryRejected.status);
}
if (!response.queryResult) {
throw new TypeError('Invalid response from server');
}
// We ignore anything but the first result
return await (0, internal_non_workflow_1.decodeFromPayloadsAtIndex)(this.dataConverter, 0, response.queryResult?.payloads);
}
async _createUpdateWorkflowRequest(lifecycleStage, input) {
const updateId = input.options?.updateId ?? (0, uuid_1.v4)();
return {
namespace: this.options.namespace,
workflowExecution: input.workflowExecution,
firstExecutionRunId: input.firstExecutionRunId,
waitPolicy: {
lifecycleStage,
},
request: {
meta: {
updateId,
identity: this.options.identity,
},
input: {
header: { fields: input.headers },
name: input.updateName,
args: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...input.args) },
},
},
};
}
/**
* Start the Update.
*
* Used as the final function of the interceptor chain during startUpdate and executeUpdate.
*/
async _startUpdateHandler(waitForStage, input) {
let waitForStageProto = (0, workflow_update_stage_1.encodeWorkflowUpdateStage)(waitForStage) ??
UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED;
waitForStageProto =
waitForStageProto >= UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED
? waitForStageProto
: UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED;
const request = await this._createUpdateWorkflowRequest(waitForStageProto, input);
// Repeatedly send UpdateWorkflowExecution until update is durable (if the server receives a request with
// an update ID that already exists, it responds with information for the existing update). If the
// requested wait stage is COMPLETED, further polling is done before returning the UpdateHandle.
let response;
try {
do {
response = await this.workflowService.updateWorkflowExecution(request);
} while (response.stage < UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED);
}
catch (err) {
this.rethrowUpdateGrpcError(err, 'Workflow Update failed', input.workflowExecution);
}
return {
updateId: request.request.meta.updateId,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowRunId: response.updateRef.workflowExecution.runId,
outcome: response.outcome ?? undefined,
};
}
/**
* Send the Update-With-Start MultiOperation request.
*
* Used as the final function of the interceptor chain during
* startUpdateWithStart and executeUpdateWithStart.
*/
async _updateWithStartHandler(waitForStage, onStart, onStartError, input) {
const startInput = {
workflowType: input.workflowType,
options: input.workflowStartOptions,
headers: input.workflowStartHeaders,
};
const updateInput = {
updateName: input.updateName,
args: input.updateArgs,
workflowExecution: {
workflowId: input.workflowStartOptions.workflowId,
},
options: input.updateOptions,
headers: input.updateHeaders,
};
let seenStart = false;
try {
const startRequest = await this.createStartWorkflowRequest(startInput);
const waitForStageProto = (0, workflow_update_stage_1.encodeWorkflowUpdateStage)(waitForStage);
const updateRequest = await this._createUpdateWorkflowRequest(waitForStageProto, updateInput);
const multiOpReq = {
namespace: this.options.namespace,
operations: [
{
startWorkflow: startRequest,
},
{
updateWorkflow: updateRequest,
},
],
};
let multiOpResp;
let startResp;
let updateResp;
let reachedStage;
// Repeatedly send ExecuteMultiOperation until update is durable (if the server receives a request with
// an update ID that already exists, it responds with information for the existing update). If the
// requested wait stage is COMPLETED, further polling is done before returning the UpdateHandle.
do {
multiOpResp = await this.workflowService.executeMultiOperation(multiOpReq);
startResp = multiOpResp.responses?.[0]
?.startWorkflow;
if (!seenStart) {
onStart(startResp);
seenStart = true;
}
updateResp = multiOpResp.responses?.[1]
?.updateWorkflow;
reachedStage =
updateResp.stage ??
UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED;
} while (reachedStage < UpdateWorkflowExecutionLifecycleStage.UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED);
return {
workflowExecution: {
workflowId: updateResp.updateRef.workflowExecution.workflowId,
runId: updateResp.updateRef.workflowExecution.runId,
},
updateId: updateRequest.request.meta.updateId,
updateOutcome: updateResp.outcome ?? undefined,
};
}
catch (thrownError) {
let err = thrownError;
if ((0, errors_1.isGrpcServiceError)(err) && err.code === grpc_js_1.status.ALREADY_EXISTS) {
err = new common_1.WorkflowExecutionAlreadyStartedError('Workflow execution already started', input.workflowStartOptions.workflowId, input.workflowType);
}
if (!seenStart) {
onStartError(err);
}
if ((0, errors_1.isGrpcServiceError)(err)) {
this.rethrowUpdateGrpcError(err, 'Update-With-Start failed', updateInput.workflowExecution);
}
throw err;
}
}
createWorkflowUpdateHandle(updateId, workflowId, workflowRunId, outcome) {
return {
updateId,
workflowId,
workflowRunId,
result: async () => {
const completedOutcome = outcome ?? (await this._pollForUpdateOutcome(updateId, { workflowId, runId: workflowRunId }));
if (completedOutcome.failure) {
throw new errors_1.WorkflowUpdateFailedError('Workflow Update failed', await (0, internal_non_workflow_1.decodeOptionalFailureToOptionalError)(this.dataConverter, completedOutcome.failure));
}
else {
return await (0, internal_non_workflow_1.decodeFromPayloadsAtIndex)(this.dataConverter, 0, completedOutcome.success?.payloads);
}
},
};
}
/**
* Poll Update until a response with an outcome is received; return that outcome.
* This is used directly; no interceptor is available.
*/
async _pollForUpdateOutcome(updateId, workflowExecution) {
const req = {
namespace: this.options.namespace,
updateRef: { workflowExecution, updateId },
identity: this.options.identity,
waitPolicy: {
lifecycleStage: (0, workflow_update_stage_1.encodeWorkflowUpdateStage)(workflow_update_stage_1.WorkflowUpdateStage.COMPLETED),
},
};
for (;;) {
try {
const response = await this.workflowService.pollWorkflowExecutionUpdate(req);
if (response.outcome) {
return response.outcome;
}
}
catch (err) {
const wE = typeof workflowExecution.workflowId === 'string' ? workflowExecution : undefined;
this.rethrowUpdateGrpcError(err, 'Workflow Update Poll failed', wE);
}
}
}
/**
* Use given input to make a signalWorkflowExecution call to the service
*
* Used as the final function of the signal interceptor chain
*/
async _signalWorkflowHandler(input) {
const req = {
identity: this.options.identity,
namespace: this.options.namespace,
workflowExecution: input.workflowExecution,
requestId: (0, uuid_1.v4)(),
// control is unused,
signalName: input.signalName,
header: { fields: input.headers },
input: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...input.args) },
};
try {
await this.workflowService.signalWorkflowExecution(req);
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to signal Workflow', input.workflowExecution);
}
}
/**
* Use given input to make a signalWithStartWorkflowExecution call to the service
*
* Used as the final function of the signalWithStart interceptor chain
*/
async _signalWithStartWorkflowHandler(input) {
const { identity } = this.options;
const { options, workflowType, signalName, signalArgs, headers } = input;
const req = {
namespace: this.options.namespace,
identity,
requestId: (0, uuid_1.v4)(),
workflowId: options.workflowId,
workflowIdReusePolicy: (0, common_1.encodeWorkflowIdReusePolicy)(options.workflowIdReusePolicy),
workflowIdConflictPolicy: (0, common_1.encodeWorkflowIdConflictPolicy)(options.workflowIdConflictPolicy),
workflowType: { name: workflowType },
input: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...options.args) },
signalName,
signalInput: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...signalArgs) },
taskQueue: {
kind: proto_1.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL,
name: options.taskQueue,
},
workflowExecutionTimeout: options.workflowExecutionTimeout,
workflowRunTimeout: options.workflowRunTimeout,
workflowTaskTimeout: options.workflowTaskTimeout,
workflowStartDelay: options.startDelay,
retryPolicy: options.retry ? (0, common_1.compileRetryPolicy)(options.retry) : undefined,
memo: options.memo ? { fields: await (0, internal_non_workflow_1.encodeMapToPayloads)(this.dataConverter, options.memo) } : undefined,
searchAttributes: options.searchAttributes || options.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: (0, payload_search_attributes_1.encodeUnifiedSearchAttributes)(options.searchAttributes, options.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
cronSchedule: options.cronSchedule,
header: { fields: headers },
userMetadata: await (0, codec_helpers_1.encodeUserMetadata)(this.dataConverter, options.staticSummary, options.staticDetails),
priority: options.priority ? (0, common_1.compilePriority)(options.priority) : undefined,
versioningOverride: options.versioningOverride ?? undefined,
};
try {
return (await this.workflowService.signalWithStartWorkflowExecution(req)).runId;
}
catch (err) {
if (err.code === grpc_js_1.status.ALREADY_EXISTS) {
throw new common_1.WorkflowExecutionAlreadyStartedError('Workflow execution already started', options.workflowId, workflowType);
}
this.rethrowGrpcError(err, 'Failed to signalWithStart Workflow', { workflowId: options.workflowId });
}
}
/**
* Use given input to make startWorkflowExecution call to the service
*
* Used as the final function of the start interceptor chain
*/
async _startWorkflowHandler(input) {
const req = await this.createStartWorkflowRequest(input);
const { options: opts, workflowType } = input;
const internalOptions = opts[internal_1.InternalWorkflowStartOptionsSymbol];
try {
const response = await this.workflowService.startWorkflowExecution(req);
if (internalOptions != null) {
internalOptions.backLink = response.link ?? undefined;
}
return response.runId;
}
catch (err) {
if (err.code === grpc_js_1.status.ALREADY_EXISTS) {
throw new common_1.WorkflowExecutionAlreadyStartedError('Workflow execution already started', opts.workflowId, workflowType);
}
this.rethrowGrpcError(err, 'Failed to start Workflow', { workflowId: opts.workflowId });
}
}
async createStartWorkflowRequest(input) {
const { options: opts, workflowType, headers } = input;
const { identity, namespace } = this.options;
const internalOptions = opts[internal_1.InternalWorkflowStartOptionsSymbol];
return {
namespace,
identity,
requestId: internalOptions?.requestId ?? (0, uuid_1.v4)(),
workflowId: opts.workflowId,
workflowIdReusePolicy: (0, common_1.encodeWorkflowIdReusePolicy)(opts.workflowIdReusePolicy),
workflowIdConflictPolicy: (0, common_1.encodeWorkflowIdConflictPolicy)(opts.workflowIdConflictPolicy),
workflowType: { name: workflowType },
input: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...opts.args) },
taskQueue: {
kind: proto_1.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL,
name: opts.taskQueue,
},
workflowExecutionTimeout: opts.workflowExecutionTimeout,
workflowRunTimeout: opts.workflowRunTimeout,
workflowTaskTimeout: opts.workflowTaskTimeout,
workflowStartDelay: opts.startDelay,
retryPolicy: opts.retry ? (0, common_1.compileRetryPolicy)(opts.retry) : undefined,
memo: opts.memo ? { fields: await (0, internal_non_workflow_1.encodeMapToPayloads)(this.dataConverter, opts.memo) } : undefined,
searchAttributes: opts.searchAttributes || opts.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: (0, payload_search_attributes_1.encodeUnifiedSearchAttributes)(opts.searchAttributes, opts.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
cronSchedule: opts.cronSchedule,
header: { fields: headers },
userMetadata: await (0, codec_helpers_1.encodeUserMetadata)(this.dataConverter, opts.staticSummary, opts.staticDetails),
priority: opts.priority ? (0, common_1.compilePriority)(opts.priority) : undefined,
versioningOverride: opts.versioningOverride ?? undefined,
...(0, internal_workflow_1.filterNullAndUndefined)(internalOptions ?? {}),
};
}
/**
* Use given input to make terminateWorkflowExecution call to the service
*
* Used as the final function of the terminate interceptor chain
*/
async _terminateWorkflowHandler(input) {
const req = {
namespace: this.options.namespace,
identity: this.options.identity,
...input,
details: {
payloads: input.details ? await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...input.details) : undefined,
},
firstExecutionRunId: input.firstExecutionRunId,
};
try {
return await this.workflowService.terminateWorkflowExecution(req);
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to terminate Workflow', input.workflowExecution);
}
}
/**
* Uses given input to make requestCancelWorkflowExecution call to the service
*
* Used as the final function of the cancel interceptor chain
*/
async _cancelWorkflowHandler(input) {
try {
return await this.workflowService.requestCancelWorkflowExecution({
namespace: this.options.namespace,
identity: this.options.identity,
requestId: (0, uuid_1.v4)(),
workflowExecution: input.workflowExecution,
firstExecutionRunId: input.firstExecutionRunId,
});
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to cancel workflow', input.workflowExecution);
}
}
/**
* Uses given input to make describeWorkflowExecution call to the service
*
* Used as the final function of the describe interceptor chain
*/
async _describeWorkflowHandler(input) {
try {
return await this.workflowService.describeWorkflowExecution({
namespace: this.options.namespace,
execution: input.workflowExecution,
});
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to describe workflow', input.workflowExecution);
}
}
/**
* Create a new workflow handle for new or existing Workflow execution
*/
_createWorkflowHandle({ workflowId, runId, firstExecutionRunId, interceptors, runIdForResult, ...resultOptions }) {
const _startUpdate = async (def, waitForStage, options) => {
const next = this._startUpdateHandler.bind(this, waitForStage);
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'startUpdate', next);
const { args, ...opts } = options ?? {};
const input = {
workflowExecution: { workflowId, runId },
firstExecutionRunId,
updateName: typeof def === 'string' ? def : def.name,
args: args ?? [],
waitForStage,
headers: {},
options: opts,
};
const output = await fn(input);
const handle = this.createWorkflowUpdateHandle(output.updateId, input.workflowExecution.workflowId, output.workflowRunId, output.outcome);
if (!output.outcome && waitForStage === workflow_update_stage_1.WorkflowUpdateStage.COMPLETED) {
await this._pollForUpdateOutcome(handle.updateId, input.workflowExecution);
}
return handle;
};
return {
client: this,
workflowId,
async result() {
return this.client.result(workflowId, runIdForResult, resultOptions);
},
async terminate(reason) {
const next = this.client._terminateWorkflowHandler.bind(this.client);
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'terminate', next);
return await fn({
workflowExecution: { workflowId, runId },
reason,
firstExecutionRunId,
});
},
async cancel() {
const next = this.client._cancelWorkflowHandler.bind(this.client);
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'cancel', next);
return await fn({
workflowExecution: { workflowId, runId },
firstExecutionRunId,
});
},
async describe() {
const next = this.client._describeWorkflowHandler.bind(this.client);
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'describe', next);
const raw = await fn({
workflowExecution: { workflowId, runId },
});
const info = await (0, helpers_1.executionInfoFromRaw)(raw.workflowExecutionInfo ?? {}, this.client.dataConverter, raw);
const userMetadata = raw.executionConfig?.userMetadata;
return {
...info,
staticDetails: async () => (await (0, internal_non_workflow_1.decodeOptionalSinglePayload)(this.client.dataConverter, userMetadata?.details)) ?? undefined,
staticSummary: async () => (await (0, internal_non_workflow_1.decodeOptionalSinglePayload)(this.client.dataConverter, userMetadata?.summary)) ?? undefined,
raw,
};
},
async fetchHistory() {
let nextPageToken = undefined;
const events = Array();
for (;;) {
const response = await this.client.workflowService.getWorkflowExecutionHistory({
nextPageToken,
namespace: this.client.options.namespace,
execution: { workflowId, runId },
});
events.push(...(response.history?.events ?? []));
nextPageToken = response.nextPageToken;
if (nextPageToken == null || nextPageToken.length === 0)
break;
}
return proto_1.temporal.api.history.v1.History.create({ events });
},
async startUpdate(def, options) {
return await _startUpdate(def, options.waitForStage, options);
},
async executeUpdate(def, options) {
const handle = await _startUpdate(def, workflow_update_stage_1.WorkflowUpdateStage.COMPLETED, options);
return await handle.result();
},
getUpdateHandle(updateId) {
return this.client.createWorkflowUpdateHandle(updateId, workflowId, runId);
},
async signal(def, ...args) {
const next = this.client._signalWorkflowHandler.bind(this.client);
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'signal', next);
await fn({
workflowExecution: { workflowId, runId },
signalName: typeof def === 'string' ? def : def.name,
args,
headers: {},
});
},
async query(def, ...args) {
const next = this.client._queryWorkflowHandler.bind(this.client);
const fn = (0, interceptors_1.composeInterceptors)(interceptors, 'query', next);
return fn({
workflowExecution: { workflowId, runId },
queryRejectCondition: (0, types_1.encodeQueryRejectCondition)(this.client.options.queryRejectCondition),
queryType: typeof def === 'string' ? def : def.name,
args,
headers: {},
});
},
};
}
/**
* Create a handle to an existing Workflow.
*
* - If only `workflowId` is passed, and there are multiple Workflow Executions with that ID, the handle will refer to
* the most recent one.
* - If `workflowId` and `runId` are passed, the handle will refer to the specific Workflow Execution with that Run
* ID.
* - If `workflowId` and {@link GetWorkflowHandleOptions.firstExecutionRunId} are passed, the handle will refer to the
* most recent Workflow Execution in the *Chain* that started with `firstExecutionRunId`.
*
* A *Chain* is a series of Workflow Executions that share the same Workflow ID and are connected by:
* - Being part of the same {@link https://docs.temporal.io/typescript/clients#scheduling-cron-workflows | Cron}
* - {@link https://docs.temporal.io/typescript/workflows#continueasnew | Continue As New}
* - {@link https://typescript.temporal.io/api/interfaces/client.workflowoptions/#retry | Retries}
*
* This method does not validate `workflowId`. If there is no Workflow Execution with the given `workflowId`, handle
* methods like `handle.describe()` will throw a {@link WorkflowNotFoundError} error.
*/
getHandle(workflowId, runId, options) {
const interceptors = this.getOrMakeInterceptors(workflowId, runId);
return this._createWorkflowHandle({
workflowId,
runId,
firstExecutionRunId: options?.firstExecutionRunId,
runIdForResult: runId ?? options?.firstExecutionRunId,
interceptors,
followRuns: options?.followRuns ?? true,
});
}
async *_list(options) {
let nextPageToken = Buffer.alloc(0);
for (;;) {
let response;
try {
response = await this.workflowService.listWorkflowExecutions({
namespace: this.options.namespace,
query: options?.query,
nextPageToken,
pageSize: options?.pageSize,
});
}
catch (e) {
this.rethrowGrpcError(e, 'Failed to list workflows', undefined);
}
// Not decoding memo payloads concurrently even though we could have to keep the lazy nature of this iterator.
// Decoding is done for `memo` fields which tend to be small.
// We might decide to change that based on user feedback.
for (const raw of response.executions) {
yield await (0, helpers_1.executionInfoFromRaw)(raw, this.dataConverter, raw);
}
nextPageToken = response.nextPageToken;
if (nextPageToken == null || nextPageToken.length === 0)
break;
}
}
/**
* Return a list of Workflow Executions matching the given `query`.
*
* Note that the list of Workflow Executions returned is approximate and eventually consistent.
*
* More info on the concept of "visibility" and the query syntax on the Temporal documentation site:
* https://docs.temporal.io/visibility
*/
list(options) {
return {
[Symbol.asyncIterator]: () => this._list(options)[Symbol.asyncIterator](),
intoHistories: (intoHistoriesOptions) => {
return (0, iterators_utils_1.mapAsyncIterable)(this._list(options), async ({ workflowId, runId }) => ({
workflowId,
history: await this.getHandle(workflowId, runId).fetchHistory(),
}), { concurrency: intoHistoriesOptions?.concurrency ?? 5 });
},
};
}
/**
* Return the number of Workflow Executions matching the given `query`. If no `query` is provided, then return the
* total number of Workflow Executions for this namespace.
*
* Note that the number of Workflow Executions returned is approximate and eventually consistent.
*
* More info on the concept of "visibility" and the query syntax on the Temporal documentation site:
* https://docs.temporal.io/visibili