UNPKG

@temporalio/client

Version:
1,304 lines (1,220 loc) 62.8 kB
import { status as grpcStatus } from '@grpc/grpc-js'; import { v4 as uuid4 } from 'uuid'; import { BaseWorkflowHandle, CancelledFailure, compileRetryPolicy, mapToPayloads, HistoryAndWorkflowId, QueryDefinition, RetryState, searchAttributePayloadConverter, SignalDefinition, UpdateDefinition, TerminatedFailure, TimeoutFailure, TimeoutType, WithWorkflowArgs, Workflow, WorkflowExecutionAlreadyStartedError, WorkflowNotFoundError, WorkflowResultType, extractWorkflowType, encodeWorkflowIdReusePolicy, decodeRetryState, encodeWorkflowIdConflictPolicy, WorkflowIdConflictPolicy, } from '@temporalio/common'; import { composeInterceptors } from '@temporalio/common/lib/interceptors'; import { History } from '@temporalio/common/lib/proto-utils'; import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; import { decodeArrayFromPayloads, decodeFromPayloadsAtIndex, decodeOptionalFailureToOptionalError, encodeMapToPayloads, encodeToPayloads, filterNullAndUndefined, } from '@temporalio/common/lib/internal-non-workflow'; import { temporal } from '@temporalio/proto'; import { ServiceError, WorkflowContinuedAsNewError, WorkflowFailedError, WorkflowUpdateFailedError, WorkflowUpdateRPCTimeoutOrCancelledError, isGrpcServiceError, } from './errors'; import { WorkflowCancelInput, WorkflowClientInterceptor, WorkflowClientInterceptors, WorkflowDescribeInput, WorkflowQueryInput, WorkflowSignalInput, WorkflowSignalWithStartInput, WorkflowStartInput, WorkflowTerminateInput, WorkflowStartUpdateInput, WorkflowStartUpdateOutput, WorkflowStartUpdateWithStartInput, WorkflowStartUpdateWithStartOutput, } from './interceptors'; import { CountWorkflowExecution, DescribeWorkflowExecutionResponse, encodeQueryRejectCondition, GetWorkflowExecutionHistoryRequest, QueryRejectCondition, RequestCancelWorkflowExecutionResponse, StartWorkflowExecutionRequest, TerminateWorkflowExecutionResponse, WorkflowExecution, WorkflowExecutionDescription, WorkflowExecutionInfo, WorkflowService, } from './types'; import { compileWorkflowOptions, WorkflowOptions, WorkflowSignalWithStartOptions, WorkflowStartOptions, WorkflowUpdateOptions, } from './workflow-options'; import { decodeCountWorkflowExecutionsResponse, executionInfoFromRaw, rethrowKnownErrorTypes } from './helpers'; import { BaseClient, BaseClientOptions, defaultBaseClientOptions, LoadedWithDefaults, WithDefaults, } from './base-client'; import { mapAsyncIterable } from './iterators-utils'; import { WorkflowUpdateStage, encodeWorkflowUpdateStage } from './workflow-update-stage'; const UpdateWorkflowExecutionLifecycleStage = temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage; /** * A client side handle to a single Workflow instance. * It can be used to start, signal, query, wait for completion, terminate and cancel a Workflow execution. * * Given the following Workflow definition: * ```ts * export const incrementSignal = defineSignal<[number]>('increment'); * export const getValueQuery = defineQuery<number>('getValue'); * export const incrementAndGetValueUpdate = defineUpdate<number, [number]>('incrementAndGetValue'); * export async function counterWorkflow(initialValue: number): Promise<void>; * ``` * * Create a handle for running and interacting with a single Workflow: * ```ts * const client = new WorkflowClient(); * // Start the Workflow with initialValue of 2. * const handle = await client.start({ * workflowType: counterWorkflow, * args: [2], * taskQueue: 'tutorial', * }); * await handle.signal(incrementSignal, 2); * const queryResult = await handle.query(getValueQuery); // 4 * const firstUpdateResult = await handle.executeUpdate(incrementAndGetValueUpdate, { args: [2] }); // 6 * const secondUpdateHandle = await handle.startUpdate(incrementAndGetValueUpdate, { args: [2] }); * const secondUpdateResult = await secondUpdateHandle.result(); // 8 * await handle.cancel(); * await handle.result(); // throws a WorkflowFailedError with `cause` set to a CancelledFailure. * ``` */ export interface WorkflowHandle<T extends Workflow = Workflow> extends BaseWorkflowHandle<T> { /** * Start an Update and wait for the result. * * @throws {@link WorkflowUpdateFailedError} if Update validation fails or if ApplicationFailure is thrown in the Update handler. * @throws {@link WorkflowUpdateRPCTimeoutOrCancelledError} if this Update call timed out or was cancelled. This doesn't * mean the update itself was timed out or cancelled. * @param def an Update definition as returned from {@link defineUpdate} * @param options Update arguments * * @example * ```ts * const updateResult = await handle.executeUpdate(incrementAndGetValueUpdate, { args: [2] }); * ``` */ executeUpdate<Ret, Args extends [any, ...any[]], Name extends string = string>( def: UpdateDefinition<Ret, Args, Name> | string, options: WorkflowUpdateOptions & { args: Args } ): Promise<Ret>; executeUpdate<Ret, Args extends [], Name extends string = string>( def: UpdateDefinition<Ret, Args, Name> | string, options?: WorkflowUpdateOptions & { args?: Args } ): Promise<Ret>; /** * Start an Update and receive a handle to the Update. The Update validator (if present) is run * before the handle is returned. * * @throws {@link WorkflowUpdateFailedError} if Update validation fails. * @throws {@link WorkflowUpdateRPCTimeoutOrCancelledError} if this Update call timed out or was cancelled. This doesn't * mean the update itself was timed out or cancelled. * * @param def an Update definition as returned from {@link defineUpdate} * @param options update arguments, and update lifecycle stage to wait for * * Currently, startUpdate always waits until a worker is accepting tasks for the workflow and the * update is accepted or rejected, and the options object must be at least * ```ts * { * waitForStage: WorkflowUpdateStage.ACCEPTED * } * ``` * If the update takes arguments, then the options object must additionally contain an `args` * property with an array of argument values. * * @example * ```ts * const updateHandle = await handle.startUpdate(incrementAndGetValueUpdate, { * args: [2], * waitForStage: 'ACCEPTED', * }); * const updateResult = await updateHandle.result(); * ``` */ startUpdate<Ret, Args extends [any, ...any[]], Name extends string = string>( def: UpdateDefinition<Ret, Args, Name> | string, options: WorkflowUpdateOptions & { args: Args; waitForStage: 'ACCEPTED'; } ): Promise<WorkflowUpdateHandle<Ret>>; startUpdate<Ret, Args extends [], Name extends string = string>( def: UpdateDefinition<Ret, Args, Name> | string, options: WorkflowUpdateOptions & { args?: Args; waitForStage: typeof WorkflowUpdateStage.ACCEPTED; } ): Promise<WorkflowUpdateHandle<Ret>>; /** * Get a handle to an Update of this Workflow. */ getUpdateHandle<Ret>(updateId: string): WorkflowUpdateHandle<Ret>; /** * Query a running or completed Workflow. * * @param def a query definition as returned from {@link defineQuery} or query name (string) * * @example * ```ts * await handle.query(getValueQuery); * await handle.query<number, []>('getValue'); * ``` */ query<Ret, Args extends any[] = []>(def: QueryDefinition<Ret, Args> | string, ...args: Args): Promise<Ret>; /** * Terminate a running Workflow */ terminate(reason?: string): Promise<TerminateWorkflowExecutionResponse>; /** * Cancel a running Workflow. * * When a Workflow is cancelled, the root scope throws {@link CancelledFailure} with `message: 'Workflow canceled'`. * That means that all cancellable scopes will throw `CancelledFailure`. * * Cancellation may be propagated to Activities depending on {@link ActivityOptions#cancellationType}, after which * Activity calls may throw an {@link ActivityFailure}, and `isCancellation(error)` will be true (see {@link isCancellation}). * * Cancellation may be propagated to Child Workflows depending on {@link ChildWorkflowOptions#cancellationType}, after * which calls to {@link executeChild} and {@link ChildWorkflowHandle#result} will throw, and `isCancellation(error)` * will be true (see {@link isCancellation}). */ cancel(): Promise<RequestCancelWorkflowExecutionResponse>; /** * Describe the current workflow execution */ describe(): Promise<WorkflowExecutionDescription>; /** * Return a workflow execution's history */ fetchHistory(): Promise<History>; /** * Readonly accessor to the underlying WorkflowClient */ readonly client: WorkflowClient; } /** * This interface is exactly the same as {@link WorkflowHandle} except it * includes the `firstExecutionRunId` returned from {@link WorkflowClient.start}. */ export interface WorkflowHandleWithFirstExecutionRunId<T extends Workflow = Workflow> extends WorkflowHandle<T> { /** * Run Id of the first Execution in the Workflow Execution Chain. */ readonly firstExecutionRunId: string; } /** * This interface is exactly the same as {@link WorkflowHandle} except it * includes the `signaledRunId` returned from `signalWithStart`. */ export interface WorkflowHandleWithSignaledRunId<T extends Workflow = Workflow> extends WorkflowHandle<T> { /** * The Run Id of the bound Workflow at the time of {@link WorkflowClient.signalWithStart}. * * Since `signalWithStart` may have signaled an existing Workflow Chain, `signaledRunId` might not be the * `firstExecutionRunId`. */ readonly signaledRunId: string; } export interface WorkflowClientOptions extends BaseClientOptions { /** * Used to override and extend default Connection functionality * * Useful for injecting auth headers and tracing Workflow executions */ // eslint-disable-next-line deprecation/deprecation interceptors?: WorkflowClientInterceptors | WorkflowClientInterceptor[]; /** * Should a query be rejected by closed and failed workflows * * @default `undefined` which means that closed and failed workflows are still queryable */ queryRejectCondition?: QueryRejectCondition; } export type LoadedWorkflowClientOptions = LoadedWithDefaults<WorkflowClientOptions>; function defaultWorkflowClientOptions(): WithDefaults<WorkflowClientOptions> { return { ...defaultBaseClientOptions(), interceptors: [], queryRejectCondition: 'NONE', }; } function assertRequiredWorkflowOptions(opts: WorkflowOptions): asserts opts is WorkflowOptions { if (!opts.taskQueue) { throw new TypeError('Missing WorkflowOptions.taskQueue'); } if (!opts.workflowId) { throw new TypeError('Missing WorkflowOptions.workflowId'); } } function ensureArgs<W extends Workflow, T extends WorkflowStartOptions<W>>( opts: T ): Omit<T, 'args'> & { args: unknown[] } { const { args, ...rest } = opts; return { args: (args as unknown[]) ?? [], ...rest }; } /** * Options for getting a result of a Workflow execution. */ export interface WorkflowResultOptions { /** * If set to true, instructs the client to follow the chain of execution before returning a Workflow's result. * * Workflow execution is chained if the Workflow has a cron schedule or continues-as-new or configured to retry * after failure or timeout. * * @default true */ followRuns?: boolean; } /** * Options for {@link WorkflowClient.getHandle} */ export interface GetWorkflowHandleOptions extends WorkflowResultOptions { /** * ID of the first execution in the Workflow execution chain. * * When getting a handle with no `runId`, pass this option to ensure some * {@link WorkflowHandle} methods (e.g. `terminate` and `cancel`) don't * affect executions from another chain. */ firstExecutionRunId?: string; } interface WorkflowHandleOptions extends GetWorkflowHandleOptions { workflowId: string; runId?: string; interceptors: WorkflowClientInterceptor[]; /** * A runId to use for getting the workflow's result. * * - When creating a handle using `getHandle`, uses the provided runId or firstExecutionRunId * - When creating a handle using `start`, uses the returned runId (first in the chain) * - When creating a handle using `signalWithStart`, uses the the returned runId */ runIdForResult?: string; } /** * An iterable list of WorkflowExecution, as returned by {@link WorkflowClient.list}. */ export interface AsyncWorkflowListIterable extends AsyncIterable<WorkflowExecutionInfo> { /** * Return an iterable of histories corresponding to this iterable's WorkflowExecutions. * Workflow histories will be fetched concurrently. * * Useful in batch replaying */ intoHistories: (intoHistoriesOptions?: IntoHistoriesOptions) => AsyncIterable<HistoryAndWorkflowId>; } /** * A client-side handle to an Update. */ export interface WorkflowUpdateHandle<Ret> { /** * The ID of this Update request. */ updateId: string; /** * The ID of the Workflow being targeted by this Update request. */ workflowId: string; /** * The ID of the Run of the Workflow being targeted by this Update request. */ workflowRunId?: string; /** * Return the result of the Update. * @throws {@link WorkflowUpdateFailedError} if ApplicationFailure is thrown in the Update handler. */ result(): Promise<Ret>; } /** * Options for {@link WorkflowHandle.getUpdateHandle} */ export interface GetWorkflowUpdateHandleOptions { /** * The ID of the Run of the Workflow targeted by the Update. */ workflowRunId?: string; } /** * Options for {@link WorkflowClient.list} */ export interface ListOptions { /** * Maximum number of results to fetch per page. * * @default depends on server config, typically 1000 */ pageSize?: number; /** * Query string for matching and ordering the results */ query?: string; } /** * Options for {@link WorkflowClient.list().intoHistories()} */ export interface IntoHistoriesOptions { /** * Maximum number of workflow histories to download concurrently. * * @default 5 */ concurrency?: number; /** * Maximum number of workflow histories to buffer ahead, ready for consumption. * * It is recommended to set `bufferLimit` to a rasonnably low number if it is expected that the * iterable may be stopped before reaching completion (for example, when implementing a fail fast * bach replay test). * * Ignored unless `concurrency > 1`. No limit applies if set to `undefined`. * * @default unlimited */ bufferLimit?: number; } const withStartWorkflowOperationResolve: unique symbol = Symbol(); const withStartWorkflowOperationReject: unique symbol = Symbol(); const withStartWorkflowOperationUsed: unique symbol = Symbol(); /** * Define how to start a workflow when using {@link WorkflowClient.startUpdateWithStart} and * {@link WorkflowClient.executeUpdateWithStart}. `workflowIdConflictPolicy` is required in the options. * * @experimental Update-with-Start is an experimental feature and may be subject to change. */ export class WithStartWorkflowOperation<T extends Workflow> { private [withStartWorkflowOperationUsed]: boolean = false; private [withStartWorkflowOperationResolve]: ((handle: WorkflowHandle<T>) => void) | undefined = undefined; private [withStartWorkflowOperationReject]: ((error: any) => void) | undefined = undefined; private workflowHandlePromise: Promise<WorkflowHandle<T>>; constructor( public workflowTypeOrFunc: string | T, public options: WorkflowStartOptions<T> & { workflowIdConflictPolicy: WorkflowIdConflictPolicy } ) { this.workflowHandlePromise = new Promise<WorkflowHandle<T>>((resolve, reject) => { this[withStartWorkflowOperationResolve] = resolve; this[withStartWorkflowOperationReject] = reject; }); } public async workflowHandle(): Promise<WorkflowHandle<T>> { return await this.workflowHandlePromise; } } /** * 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. */ export class WorkflowClient extends BaseClient { public readonly options: LoadedWorkflowClientOptions; constructor(options?: WorkflowClientOptions) { super(options); this.options = { ...defaultWorkflowClientOptions(), ...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(): WorkflowService { return this.connection.workflowService; } protected async _start<T extends Workflow>( workflowTypeOrFunc: string | T, options: WithWorkflowArgs<T, WorkflowOptions>, interceptors: WorkflowClientInterceptor[] ): Promise<string> { const workflowType = extractWorkflowType(workflowTypeOrFunc); assertRequiredWorkflowOptions(options); const compiledOptions = compileWorkflowOptions(ensureArgs(options)); const start = composeInterceptors(interceptors, 'start', this._startWorkflowHandler.bind(this)); return start({ options: compiledOptions, headers: {}, workflowType, }); } protected async _signalWithStart<T extends Workflow, SA extends any[]>( workflowTypeOrFunc: string | T, options: WithWorkflowArgs<T, WorkflowSignalWithStartOptions<SA>>, interceptors: WorkflowClientInterceptor[] ): Promise<string> { const workflowType = extractWorkflowType(workflowTypeOrFunc); const { signal, signalArgs, ...rest } = options; assertRequiredWorkflowOptions(rest); const compiledOptions = compileWorkflowOptions(ensureArgs(rest)); const signalWithStart = 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 */ public async start<T extends Workflow>( workflowTypeOrFunc: string | T, options: WorkflowStartOptions<T> ): Promise<WorkflowHandleWithFirstExecutionRunId<T>> { 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, }) as WorkflowHandleWithFirstExecutionRunId<T>; // Cast is safe because we know we add the firstExecutionRunId below (handle as any) /* 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 */ public async signalWithStart<WorkflowFn extends Workflow, SignalArgs extends any[] = []>( workflowTypeOrFunc: string | WorkflowFn, options: WithWorkflowArgs<WorkflowFn, WorkflowSignalWithStartOptions<SignalArgs>> ): Promise<WorkflowHandleWithSignaledRunId<WorkflowFn>> { 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, }) as WorkflowHandleWithSignaledRunId<WorkflowFn>; // Cast is safe because we know we add the signaledRunId below (handle as any) /* 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 * * @experimental Update-with-Start is an experimental feature and may be subject to change. */ public async executeUpdateWithStart<T extends Workflow, Ret, Args extends any[]>( updateDef: UpdateDefinition<Ret, Args> | string, updateOptions: WorkflowUpdateOptions & { args?: Args; startWorkflowOperation: WithStartWorkflowOperation<T> } ): Promise<Ret> { const handle = await this._startUpdateWithStart(updateDef, { ...updateOptions, waitForStage: 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 * * @experimental Update-with-Start is an experimental feature and may be subject to change. */ public async startUpdateWithStart<T extends Workflow, Ret, Args extends any[]>( updateDef: UpdateDefinition<Ret, Args> | string, updateOptions: WorkflowUpdateOptions & { args?: Args; waitForStage: 'ACCEPTED'; startWorkflowOperation: WithStartWorkflowOperation<T>; } ): Promise<WorkflowUpdateHandle<Ret>> { return this._startUpdateWithStart(updateDef, updateOptions); } protected async _startUpdateWithStart<T extends Workflow, Ret, Args extends any[]>( updateDef: UpdateDefinition<Ret, Args> | string, updateWithStartOptions: WorkflowUpdateOptions & { args?: Args; waitForStage: WorkflowUpdateStage; startWorkflowOperation: WithStartWorkflowOperation<T>; } ): Promise<WorkflowUpdateHandle<Ret>> { 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: WorkflowStartUpdateWithStartInput = { workflowType: extractWorkflowType(workflowTypeOrFunc), workflowStartOptions: compileWorkflowOptions(ensureArgs(workflowOptions)), workflowStartHeaders: {}, updateName: typeof updateDef === 'string' ? updateDef : updateDef.name, updateArgs: args ?? [], updateOptions, updateHeaders: {}, }; const interceptors = this.getOrMakeInterceptors(workflowId); const onStart = (startResponse: temporal.api.workflowservice.v1.IStartWorkflowExecutionResponse) => startWorkflowOperation[withStartWorkflowOperationResolve]!( this._createWorkflowHandle({ workflowId, firstExecutionRunId: startResponse.runId ?? undefined, interceptors, followRuns: workflowOptions.followRuns ?? true, }) ); const onStartError = (err: any) => { startWorkflowOperation[withStartWorkflowOperationReject]!(err); }; const fn = composeInterceptors( interceptors, 'startUpdateWithStart', this._updateWithStartHandler.bind(this, waitForStage, onStart, onStartError) ); const updateOutput = await fn(startUpdateWithStartInput); let outcome = updateOutput.updateOutcome; if (!outcome && waitForStage === WorkflowUpdateStage.COMPLETED) { outcome = await this._pollForUpdateOutcome(updateOutput.updateId, { workflowId, runId: updateOutput.workflowExecution.runId, }); } return this.createWorkflowUpdateHandle<Ret>( 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 */ public async execute<T extends Workflow>( workflowTypeOrFunc: string | T, options: WorkflowStartOptions<T> ): Promise<WorkflowResultType<T>> { 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. */ public async result<T extends Workflow>( workflowId: string, runId?: string, opts?: WorkflowResultOptions ): Promise<WorkflowResultType<T>> { const followRuns = opts?.followRuns ?? true; const execution: temporal.api.common.v1.IWorkflowExecution = { workflowId, runId }; const req: GetWorkflowExecutionHistoryRequest = { namespace: this.options.namespace, execution, skipArchival: true, waitNewEvent: true, historyEventFilterType: temporal.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, }; let ev: temporal.api.history.v1.IHistoryEvent; for (;;) { let res: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse; 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 decodeArrayFromPayloads( this.dataConverter, ev.workflowExecutionCompletedEventAttributes.result?.payloads ); return result as any; } 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 WorkflowFailedError( 'Workflow execution failed', await decodeOptionalFailureToOptionalError(this.dataConverter, failure), decodeRetryState(retryState) ); } else if (ev.workflowExecutionCanceledEventAttributes) { const failure = new CancelledFailure( 'Workflow canceled', await decodeArrayFromPayloads( this.dataConverter, ev.workflowExecutionCanceledEventAttributes.details?.payloads ) ); failure.stack = ''; throw new WorkflowFailedError('Workflow execution cancelled', failure, RetryState.NON_RETRYABLE_FAILURE); } else if (ev.workflowExecutionTerminatedEventAttributes) { const failure = new TerminatedFailure( ev.workflowExecutionTerminatedEventAttributes.reason || 'Workflow execution terminated' ); failure.stack = ''; throw new WorkflowFailedError( ev.workflowExecutionTerminatedEventAttributes.reason || 'Workflow execution terminated', failure, 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 TimeoutFailure('Workflow execution timed out', undefined, TimeoutType.START_TO_CLOSE); failure.stack = ''; throw new WorkflowFailedError( 'Workflow execution timed out', failure, 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 WorkflowContinuedAsNewError('Workflow execution continued as new', newExecutionRunId); } execution.runId = newExecutionRunId; req.nextPageToken = undefined; continue; } } } protected rethrowUpdateGrpcError( err: unknown, fallbackMessage: string, workflowExecution?: WorkflowExecution ): never { if (isGrpcServiceError(err)) { if (err.code === grpcStatus.DEADLINE_EXCEEDED || err.code === grpcStatus.CANCELLED) { throw new WorkflowUpdateRPCTimeoutOrCancelledError(err.details ?? 'Workflow update call timeout or cancelled', { cause: err, }); } } if (err instanceof CancelledFailure) { throw new WorkflowUpdateRPCTimeoutOrCancelledError(err.message ?? 'Workflow update call timeout or cancelled', { cause: err, }); } this.rethrowGrpcError(err, fallbackMessage, workflowExecution); } protected rethrowGrpcError(err: unknown, fallbackMessage: string, workflowExecution?: WorkflowExecution): never { if (isGrpcServiceError(err)) { rethrowKnownErrorTypes(err); if (err.code === grpcStatus.NOT_FOUND) { throw new WorkflowNotFoundError( err.details ?? 'Workflow not found', workflowExecution?.workflowId ?? '', workflowExecution?.runId ); } throw new ServiceError(fallbackMessage, { cause: err }); } throw new ServiceError('Unexpected error while making gRPC request', { cause: err as Error }); } /** * Use given input to make a queryWorkflow call to the service * * Used as the final function of the query interceptor chain */ protected async _queryWorkflowHandler(input: WorkflowQueryInput): Promise<unknown> { const req: temporal.api.workflowservice.v1.IQueryWorkflowRequest = { queryRejectCondition: input.queryRejectCondition, namespace: this.options.namespace, execution: input.workflowExecution, query: { queryType: input.queryType, queryArgs: { payloads: await encodeToPayloads(this.dataConverter, ...input.args) }, header: { fields: input.headers }, }, }; let response: temporal.api.workflowservice.v1.QueryWorkflowResponse; try { response = await this.workflowService.queryWorkflow(req); } catch (err) { if (isGrpcServiceError(err)) { rethrowKnownErrorTypes(err); if (err.code === grpcStatus.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 decodeFromPayloadsAtIndex(this.dataConverter, 0, response.queryResult?.payloads); } protected async _createUpdateWorkflowRequest( lifecycleStage: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage, input: WorkflowStartUpdateInput ): Promise<temporal.api.workflowservice.v1.IUpdateWorkflowExecutionRequest> { const updateId = input.options?.updateId ?? uuid4(); 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 encodeToPayloads(this.dataConverter, ...input.args) }, }, }, }; } /** * Start the Update. * * Used as the final function of the interceptor chain during startUpdate and executeUpdate. */ protected async _startUpdateHandler( waitForStage: WorkflowUpdateStage, input: WorkflowStartUpdateInput ): Promise<WorkflowStartUpdateOutput> { let waitForStageProto: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage = 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: temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse; 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. */ protected async _updateWithStartHandler( waitForStage: WorkflowUpdateStage, onStart: (startResponse: temporal.api.workflowservice.v1.IStartWorkflowExecutionResponse) => void, onStartError: (err: any) => void, input: WorkflowStartUpdateWithStartInput ): Promise<WorkflowStartUpdateWithStartOutput> { const startInput: WorkflowStartInput = { workflowType: input.workflowType, options: input.workflowStartOptions, headers: input.workflowStartHeaders, }; const updateInput: WorkflowStartUpdateInput = { 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 = encodeWorkflowUpdateStage(waitForStage)!; const updateRequest = await this._createUpdateWorkflowRequest(waitForStageProto, updateInput); const multiOpReq: temporal.api.workflowservice.v1.IExecuteMultiOperationRequest = { namespace: this.options.namespace, operations: [ { startWorkflow: startRequest, }, { updateWorkflow: updateRequest, }, ], }; let multiOpResp: temporal.api.workflowservice.v1.IExecuteMultiOperationResponse; let startResp: temporal.api.workflowservice.v1.IStartWorkflowExecutionResponse; let updateResp: temporal.api.workflowservice.v1.IUpdateWorkflowExecutionResponse; let reachedStage: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage; // 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 as temporal.api.workflowservice.v1.IStartWorkflowExecutionResponse; if (!seenStart) { onStart(startResp); seenStart = true; } updateResp = multiOpResp.responses?.[1] ?.updateWorkflow as temporal.api.workflowservice.v1.IUpdateWorkflowExecutionResponse; 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 (isGrpcServiceError(err) && err.code === grpcStatus.ALREADY_EXISTS) { err = new WorkflowExecutionAlreadyStartedError( 'Workflow execution already started', input.workflowStartOptions.workflowId, input.workflowType ); } if (!seenStart) { onStartError(err); } if (isGrpcServiceError(err)) { this.rethrowUpdateGrpcError(err, 'Update-With-Start failed', updateInput.workflowExecution); } throw err; } } protected createWorkflowUpdateHandle<Ret>( updateId: string, workflowId: string, workflowRunId?: string, outcome?: temporal.api.update.v1.IOutcome ): WorkflowUpdateHandle<Ret> { return { updateId, workflowId, workflowRunId, result: async () => { const completedOutcome = outcome ?? (await this._pollForUpdateOutcome(updateId, { workflowId, runId: workflowRunId })); if (completedOutcome.failure) { throw new WorkflowUpdateFailedError( 'Workflow Update failed', await decodeOptionalFailureToOptionalError(this.dataConverter, completedOutcome.failure) ); } else { return await decodeFromPayloadsAtIndex<Ret>(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. */ protected async _pollForUpdateOutcome( updateId: string, workflowExecution: temporal.api.common.v1.IWorkflowExecution ): Promise<temporal.api.update.v1.IOutcome> { const req: temporal.api.workflowservice.v1.IPollWorkflowExecutionUpdateRequest = { namespace: this.options.namespace, updateRef: { workflowExecution, updateId }, identity: this.options.identity, waitPolicy: { lifecycleStage: encodeWorkflowUpdateStage(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 as WorkflowExecution); } } } /** * Use given input to make a signalWorkflowExecution call to the service * * Used as the final function of the signal interceptor chain */ protected async _signalWorkflowHandler(input: WorkflowSignalInput): Promise<void> { const req: temporal.api.workflowservice.v1.ISignalWorkflowExecutionRequest = { identity: this.options.identity, namespace: this.options.namespace, workflowExecution: input.workflowExecution, requestId: uuid4(), // control is unused, signalName: input.signalName, header: { fields: input.headers }, input: { payloads: await 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 */ protected async _signalWithStartWorkflowHandler(input: WorkflowSignalWithStartInput): Promise<string> { const { identity } = this.options; const { options, workflowType, signalName, signalArgs, headers } = input; const req: temporal.api.workflowservice.v1.ISignalWithStartWorkflowExecutionRequest = { namespace: this.options.namespace, identity, requestId: uuid4(), workflowId: options.workflowId, workflowIdReusePolicy: encodeWorkflowIdReusePolicy(options.workflowIdReusePolicy), workflowIdConflictPolicy: encodeWorkflowIdConflictPolicy(options.workflowIdConflictPolicy), workflowType: { name: workflowType }, input: { payloads: await encodeToPayloads(this.dataConverter, ...options.args) }, signalName, signalInput: { payloads: await encodeToPayloads(this.dataConverter, ...signalArgs) }, taskQueue: { kind: 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 ? compileRetryPolicy(options.retry) : undefined, memo: options.memo ? { fields: await encodeMapToPayloads(this.dataConverter, options.memo) } : undefined, searchAttributes: options.searchAttributes ? { indexedFields: mapToPayloads(searchAttributePayloadConverter, options.searchAttributes), } : undefined, cronSchedule: options.cronSchedule, header: { fields: headers }, }; try { return (await this.workflowService.signalWithStartWorkflowExecution(req)).runId; } catch (err: any) { if (err.code === grpcStatus.ALREADY_EXISTS) { throw new 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 */ protected async _startWorkflowHandler(input: WorkflowStartInput): Promise<string> { const req = await this.createStartWorkflowRequest(input); const { options: opts, workflowType } = input; try { return (await this.workflowService.startWorkflowExecution(req)).runId; } catch (err: any) { if (err.code === grpcStatus.ALREADY_EXISTS) { throw new WorkflowExecutionAlreadyStartedError( 'Workflow execution already started', opts.workflowId, workflowType ); } this.rethrowGrpcError(err, 'Failed to start Workflow', { workflowId: opts.workflowId }); } } protected async createStartWorkflowRequest(input: WorkflowStartInput): Promise<StartWorkflowExecutionRequest> { const { options: opts, workflowType, headers } = input; const { identity, namespace } = this.options; return { namespace, identity, requestId: uuid4(), workflowId: opts.workflowId, workflowIdReusePolicy: encodeWorkflowIdReusePolicy(opts.workflowIdReusePolicy), workflowIdConflictPolicy: encodeWorkflowIdConflictPolicy(opts.workflowIdConflictPolicy), workflowType: { name: workflowType }, input: { payloads: await encodeToPayloads(this.dataConverter, ...opts.args) }, taskQueue: { kind: 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 ? compileRetryPolicy(opts.retry) : undefined, memo: opts.memo ? { fields: await encodeMapToPayloads(this.dataConverter, opts.memo) } : undefined, searchAttributes: opts.searchAttributes ? { indexedFields: mapToPayloads(searchAttributePayloadConverter, opts.searchAttributes), } : undefined, cronSchedule: opts.cronSchedule, header: { fields: headers }, }; } /** * Use given input to make terminateWorkflowExecution call to the service * * Used as the final function of the terminate interceptor chain */ protected async _terminateWorkflowHandler( input: WorkflowTerminateInput ): Promise<TerminateWorkflowExecutionResponse