@temporalio/workflow
Version:
Temporal.io SDK Workflow sub-package
1,344 lines (1,269 loc) • 66.6 kB
text/typescript
import {
ActivityFunction,
ActivityOptions,
compileRetryPolicy,
compilePriority,
encodeActivityCancellationType,
encodeWorkflowIdReusePolicy,
extractWorkflowType,
HandlerUnfinishedPolicy,
LocalActivityOptions,
mapToPayloads,
QueryDefinition,
SearchAttributes,
SearchAttributeValue,
SignalDefinition,
toPayloads,
TypedSearchAttributes,
UntypedActivities,
UpdateDefinition,
WithWorkflowArgs,
Workflow,
WorkflowResultType,
WorkflowReturnType,
WorkflowUpdateValidatorType,
SearchAttributeUpdatePair,
WorkflowDefinitionOptionsOrGetter,
} from '@temporalio/common';
import { userMetadataToPayload } from '@temporalio/common/lib/user-metadata';
import {
encodeUnifiedSearchAttributes,
searchAttributePayloadConverter,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { versioningIntentToProto } from '@temporalio/common/lib/versioning-intent-enum';
import { Duration, msOptionalToTs, msToNumber, msToTs, requiredTsToMs } from '@temporalio/common/lib/time';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { temporal } from '@temporalio/proto';
import { deepMerge } from '@temporalio/common/lib/internal-workflow';
import { throwIfReservedName } from '@temporalio/common/lib/reserved';
import { CancellationScope, registerSleepImplementation } from './cancellation-scope';
import { UpdateScope } from './update-scope';
import {
ActivityInput,
LocalActivityInput,
SignalWorkflowInput,
StartChildWorkflowExecutionInput,
TimerInput,
TimerOptions,
} from './interceptors';
import {
ChildWorkflowCancellationType,
ChildWorkflowOptions,
ChildWorkflowOptionsWithDefaults,
ContinueAsNew,
ContinueAsNewOptions,
DefaultSignalHandler,
EnhancedStackTrace,
Handler,
QueryHandlerOptions,
SignalHandlerOptions,
UpdateHandlerOptions,
WorkflowInfo,
UpdateInfo,
encodeChildWorkflowCancellationType,
encodeParentClosePolicy,
DefaultUpdateHandler,
DefaultQueryHandler,
} from './interfaces';
import { LocalActivityDoBackoff } from './errors';
import { assertInWorkflowContext, getActivator, maybeGetActivator } from './global-attributes';
import { untrackPromise } from './stack-helpers';
import { ChildWorkflowHandle, ExternalWorkflowHandle } from './workflow-handle';
// Avoid a circular dependency
registerSleepImplementation(sleep);
/**
* Adds default values of `workflowId` and `cancellationType` to given workflow options.
*/
export function addDefaultWorkflowOptions<T extends Workflow>(
opts: WithWorkflowArgs<T, ChildWorkflowOptions>
): ChildWorkflowOptionsWithDefaults {
const { args, workflowId, ...rest } = opts;
return {
workflowId: workflowId ?? uuid4(),
args: (args ?? []) as unknown[],
cancellationType: ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
...rest,
};
}
/**
* Push a startTimer command into state accumulator and register completion
*/
function timerNextHandler({ seq, durationMs, options }: TimerInput) {
const activator = getActivator();
return new Promise<void>((resolve, reject) => {
const scope = CancellationScope.current();
if (scope.consideredCancelled) {
untrackPromise(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch((err) => {
if (!activator.completions.timer.delete(seq)) {
return; // Already resolved or never scheduled
}
activator.pushCommand({
cancelTimer: {
seq,
},
});
reject(err);
})
);
}
activator.pushCommand({
startTimer: {
seq,
startToFireTimeout: msToTs(durationMs),
},
userMetadata: userMetadataToPayload(activator.payloadConverter, options?.summary, undefined),
});
activator.completions.timer.set(seq, {
resolve,
reject,
});
});
}
/**
* Asynchronous sleep.
*
* Schedules a timer on the Temporal service.
*
* @param ms sleep duration - number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}.
* If given a negative number or 0, value will be set to 1.
* @param options optional timer options for additional configuration
*/
export function sleep(ms: Duration, options?: TimerOptions): Promise<void> {
const activator = assertInWorkflowContext('Workflow.sleep(...) may only be used from a Workflow Execution');
const seq = activator.nextSeqs.timer++;
const durationMs = Math.max(1, msToNumber(ms));
const execute = composeInterceptors(activator.interceptors.outbound, 'startTimer', timerNextHandler);
return execute({
durationMs,
seq,
options,
});
}
function validateActivityOptions(options: ActivityOptions): void {
if (options.scheduleToCloseTimeout === undefined && options.startToCloseTimeout === undefined) {
throw new TypeError('Required either scheduleToCloseTimeout or startToCloseTimeout');
}
}
// Use same validation we use for normal activities
const validateLocalActivityOptions = validateActivityOptions;
/**
* Push a scheduleActivity command into activator accumulator and register completion
*/
function scheduleActivityNextHandler({ options, args, headers, seq, activityType }: ActivityInput): Promise<unknown> {
const activator = getActivator();
validateActivityOptions(options);
return new Promise((resolve, reject) => {
const scope = CancellationScope.current();
if (scope.consideredCancelled) {
untrackPromise(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch(() => {
if (!activator.completions.activity.has(seq)) {
return; // Already resolved or never scheduled
}
activator.pushCommand({
requestCancelActivity: {
seq,
},
});
})
);
}
activator.pushCommand({
scheduleActivity: {
seq,
activityId: options.activityId ?? `${seq}`,
activityType,
arguments: toPayloads(activator.payloadConverter, ...args),
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
taskQueue: options.taskQueue || activator.info.taskQueue,
heartbeatTimeout: msOptionalToTs(options.heartbeatTimeout),
scheduleToCloseTimeout: msOptionalToTs(options.scheduleToCloseTimeout),
startToCloseTimeout: msOptionalToTs(options.startToCloseTimeout),
scheduleToStartTimeout: msOptionalToTs(options.scheduleToStartTimeout),
headers,
cancellationType: encodeActivityCancellationType(options.cancellationType),
doNotEagerlyExecute: !(options.allowEagerDispatch ?? true),
versioningIntent: versioningIntentToProto(options.versioningIntent), // eslint-disable-line deprecation/deprecation
priority: options.priority ? compilePriority(options.priority) : undefined,
},
userMetadata: userMetadataToPayload(activator.payloadConverter, options.summary, undefined),
});
activator.completions.activity.set(seq, {
resolve,
reject,
});
});
}
/**
* Push a scheduleActivity command into state accumulator and register completion
*/
async function scheduleLocalActivityNextHandler({
options,
args,
headers,
seq,
activityType,
attempt,
originalScheduleTime,
}: LocalActivityInput): Promise<unknown> {
const activator = getActivator();
// Eagerly fail the local activity (which will in turn fail the workflow task.
// Do not fail on replay where the local activities may not be registered on the replay worker.
if (!activator.info.unsafe.isReplaying && !activator.registeredActivityNames.has(activityType)) {
throw new ReferenceError(`Local activity of type '${activityType}' not registered on worker`);
}
validateLocalActivityOptions(options);
return new Promise((resolve, reject) => {
const scope = CancellationScope.current();
if (scope.consideredCancelled) {
untrackPromise(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch(() => {
if (!activator.completions.activity.has(seq)) {
return; // Already resolved or never scheduled
}
activator.pushCommand({
requestCancelLocalActivity: {
seq,
},
});
})
);
}
activator.pushCommand({
scheduleLocalActivity: {
seq,
attempt,
originalScheduleTime,
// Intentionally not exposing activityId as an option
activityId: `${seq}`,
activityType,
arguments: toPayloads(activator.payloadConverter, ...args),
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
scheduleToCloseTimeout: msOptionalToTs(options.scheduleToCloseTimeout),
startToCloseTimeout: msOptionalToTs(options.startToCloseTimeout),
scheduleToStartTimeout: msOptionalToTs(options.scheduleToStartTimeout),
localRetryThreshold: msOptionalToTs(options.localRetryThreshold),
headers,
cancellationType: encodeActivityCancellationType(options.cancellationType),
},
userMetadata: userMetadataToPayload(activator.payloadConverter, options.summary, undefined),
});
activator.completions.activity.set(seq, {
resolve,
reject,
});
});
}
/**
* Schedule an activity and run outbound interceptors
* @hidden
*/
export function scheduleActivity<R>(activityType: string, args: any[], options: ActivityOptions): Promise<R> {
const activator = assertInWorkflowContext(
'Workflow.scheduleActivity(...) may only be used from a Workflow Execution'
);
if (options === undefined) {
throw new TypeError('Got empty activity options');
}
const seq = activator.nextSeqs.activity++;
const execute = composeInterceptors(activator.interceptors.outbound, 'scheduleActivity', scheduleActivityNextHandler);
return execute({
activityType,
headers: {},
options,
args,
seq,
}) as Promise<R>;
}
/**
* Schedule an activity and run outbound interceptors
* @hidden
*/
export async function scheduleLocalActivity<R>(
activityType: string,
args: any[],
options: LocalActivityOptions
): Promise<R> {
const activator = assertInWorkflowContext(
'Workflow.scheduleLocalActivity(...) may only be used from a Workflow Execution'
);
if (options === undefined) {
throw new TypeError('Got empty activity options');
}
let attempt = 1;
let originalScheduleTime = undefined;
for (;;) {
const seq = activator.nextSeqs.activity++;
const execute = composeInterceptors(
activator.interceptors.outbound,
'scheduleLocalActivity',
scheduleLocalActivityNextHandler
);
try {
return (await execute({
activityType,
headers: {},
options,
args,
seq,
attempt,
originalScheduleTime,
})) as Promise<R>;
} catch (err) {
if (err instanceof LocalActivityDoBackoff) {
await sleep(requiredTsToMs(err.backoff.backoffDuration, 'backoffDuration'));
if (typeof err.backoff.attempt !== 'number') {
throw new TypeError('Invalid backoff attempt type');
}
attempt = err.backoff.attempt;
originalScheduleTime = err.backoff.originalScheduleTime ?? undefined;
} else {
throw err;
}
}
}
}
function startChildWorkflowExecutionNextHandler({
options,
headers,
workflowType,
seq,
}: StartChildWorkflowExecutionInput): Promise<[Promise<string>, Promise<unknown>]> {
const activator = getActivator();
const workflowId = options.workflowId ?? uuid4();
const startPromise = new Promise<string>((resolve, reject) => {
const scope = CancellationScope.current();
if (scope.consideredCancelled) {
untrackPromise(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch(() => {
const complete = !activator.completions.childWorkflowComplete.has(seq);
if (!complete) {
activator.pushCommand({
cancelChildWorkflowExecution: { childWorkflowSeq: seq },
});
}
// Nothing to cancel otherwise
})
);
}
activator.pushCommand({
startChildWorkflowExecution: {
seq,
workflowId,
workflowType,
input: toPayloads(activator.payloadConverter, ...options.args),
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
taskQueue: options.taskQueue || activator.info.taskQueue,
workflowExecutionTimeout: msOptionalToTs(options.workflowExecutionTimeout),
workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
namespace: activator.info.namespace, // Not configurable
headers,
cancellationType: encodeChildWorkflowCancellationType(options.cancellationType),
workflowIdReusePolicy: encodeWorkflowIdReusePolicy(options.workflowIdReusePolicy),
parentClosePolicy: encodeParentClosePolicy(options.parentClosePolicy),
cronSchedule: options.cronSchedule,
searchAttributes:
options.searchAttributes || options.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? encodeUnifiedSearchAttributes(options.searchAttributes, options.typedSearchAttributes) // eslint-disable-line deprecation/deprecation
: undefined,
memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo),
versioningIntent: versioningIntentToProto(options.versioningIntent), // eslint-disable-line deprecation/deprecation
priority: options.priority ? compilePriority(options.priority) : undefined,
},
userMetadata: userMetadataToPayload(activator.payloadConverter, options?.staticSummary, options?.staticDetails),
});
activator.completions.childWorkflowStart.set(seq, {
resolve,
reject,
});
});
// We construct a Promise for the completion of the child Workflow before we know
// if the Workflow code will await it to capture the result in case it does.
const completePromise = new Promise((resolve, reject) => {
// Chain start Promise rejection to the complete Promise.
untrackPromise(startPromise.catch(reject));
activator.completions.childWorkflowComplete.set(seq, {
resolve,
reject,
});
});
untrackPromise(startPromise);
untrackPromise(completePromise);
// Prevent unhandled rejection because the completion might not be awaited
untrackPromise(completePromise.catch(() => undefined));
const ret = new Promise<[Promise<string>, Promise<unknown>]>((resolve) => resolve([startPromise, completePromise]));
untrackPromise(ret);
return ret;
}
function signalWorkflowNextHandler({ seq, signalName, args, target, headers }: SignalWorkflowInput) {
const activator = getActivator();
return new Promise<any>((resolve, reject) => {
const scope = CancellationScope.current();
if (scope.consideredCancelled) {
untrackPromise(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch(() => {
if (!activator.completions.signalWorkflow.has(seq)) {
return;
}
activator.pushCommand({ cancelSignalWorkflow: { seq } });
})
);
}
activator.pushCommand({
signalExternalWorkflowExecution: {
seq,
args: toPayloads(activator.payloadConverter, ...args),
headers,
signalName,
...(target.type === 'external'
? {
workflowExecution: {
namespace: activator.info.namespace,
...target.workflowExecution,
},
}
: {
childWorkflowId: target.childWorkflowId,
}),
},
});
activator.completions.signalWorkflow.set(seq, { resolve, reject });
});
}
/**
* Symbol used in the return type of proxy methods to mark that an attribute on the source type is not a method.
*
* @see {@link ActivityInterfaceFor}
* @see {@link proxyActivities}
* @see {@link proxyLocalActivities}
*/
export const NotAnActivityMethod = Symbol.for('__TEMPORAL_NOT_AN_ACTIVITY_METHOD');
/**
* Type helper that takes a type `T` and transforms attributes that are not {@link ActivityFunction} to
* {@link NotAnActivityMethod}.
*
* @example
*
* Used by {@link proxyActivities} to get this compile-time error:
*
* ```ts
* interface MyActivities {
* valid(input: number): Promise<number>;
* invalid(input: number): number;
* }
*
* const act = proxyActivities<MyActivities>({ startToCloseTimeout: '5m' });
*
* await act.valid(true);
* await act.invalid();
* // ^ TS complains with:
* // (property) invalidDefinition: typeof NotAnActivityMethod
* // This expression is not callable.
* // Type 'Symbol' has no call signatures.(2349)
* ```
*/
export type ActivityInterfaceFor<T> = {
[K in keyof T]: T[K] extends ActivityFunction ? ActivityFunctionWithOptions<T[K]> : typeof NotAnActivityMethod;
};
export type ActivityFunctionWithOptions<T extends ActivityFunction> = T & {
/**
* Execute the activity, overriding its existing options with the
* provided options.
*
* @param options ActivityOptions
* @param args: list of arguments
* @returns return value of the activity
*
* @experimental executeWithOptions is a new method to provide call-site options and is subject to change
*/
executeWithOptions(options: ActivityOptions, args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
};
/**
* The local activity counterpart to {@link ActivityInterfaceFor}
*/
export type LocalActivityInterfaceFor<T> = {
[K in keyof T]: T[K] extends ActivityFunction ? LocalActivityFunctionWithOptions<T[K]> : typeof NotAnActivityMethod;
};
export type LocalActivityFunctionWithOptions<T extends ActivityFunction> = T & {
/**
* Run the local activity, overriding its existing options with the
* provided options.
*
* @param options LocalActivityOptions
* @param args: list of arguments
* @returns return value of the activity
*
* @experimental executeWithOptions is a new method to provide call-site options and is subject to change
*/
executeWithOptions(options: LocalActivityOptions, args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
};
/**
* Configure Activity functions with given {@link ActivityOptions}.
*
* This method may be called multiple times to setup Activities with different options.
*
* @return a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy | Proxy} for
* which each attribute is a callable Activity function
*
* @example
* ```ts
* import { proxyActivities } from '@temporalio/workflow';
* import * as activities from '../activities';
*
* // Setup Activities from module exports
* const { httpGet, otherActivity } = proxyActivities<typeof activities>({
* startToCloseTimeout: '30 minutes',
* });
*
* // Use activities with default options
* const result1 = await httpGet('http://example.com');
*
* // Override options for specific activity calls
* const result2 = await httpGet.executeWithOptions({
* staticSummary: 'Fetches data from external API',
* scheduleToCloseTimeout: '5m'
* }, ['http://api.example.com']);
*
* const result3 = await otherActivity.executeWithOptions({
* staticSummary: 'Processes the fetched data',
* taskQueue: 'special-task-queue'
* }, [data]);
*
* // Setup Activities from an explicit interface (e.g. when defined by another SDK)
* interface JavaActivities {
* httpGetFromJava(url: string): Promise<string>
* someOtherJavaActivity(arg1: number, arg2: string): Promise<string>;
* }
*
* const {
* httpGetFromJava,
* someOtherJavaActivity
* } = proxyActivities<JavaActivities>({
* taskQueue: 'java-worker-taskQueue',
* startToCloseTimeout: '5m',
* });
*
* export function execute(): Promise<void> {
* const response = await httpGet("http://example.com");
* // Or with custom options:
* const response2 = await httpGetFromJava.executeWithOptions({
* staticSummary: 'Java HTTP call with timeout override',
* startToCloseTimeout: '2m'
* }, ["http://fast-api.example.com"]);
* // ...
* }
* ```
*/
export function proxyActivities<A = UntypedActivities>(options: ActivityOptions): ActivityInterfaceFor<A> {
if (options === undefined) {
throw new TypeError('options must be defined');
}
// Validate as early as possible for immediate user feedback
validateActivityOptions(options);
return new Proxy({} as ActivityInterfaceFor<A>, {
get(_, activityType) {
if (typeof activityType !== 'string') {
throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`);
}
function activityProxyFunction(...args: unknown[]): Promise<unknown> {
return scheduleActivity(activityType as string, args, options);
}
activityProxyFunction.executeWithOptions = function (
overrideOptions: ActivityOptions,
args: any[]
): Promise<unknown> {
return scheduleActivity(activityType, args, deepMerge(options, overrideOptions));
};
return activityProxyFunction;
},
});
}
/**
* Configure Local Activity functions with given {@link LocalActivityOptions}.
*
* This method may be called multiple times to setup Activities with different options.
*
* @return a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy | Proxy}
* for which each attribute is a callable Activity function
*
* @see {@link proxyActivities} for examples
*/
export function proxyLocalActivities<A = UntypedActivities>(
options: LocalActivityOptions
): LocalActivityInterfaceFor<A> {
if (options === undefined) {
throw new TypeError('options must be defined');
}
// Validate as early as possible for immediate user feedback
validateLocalActivityOptions(options);
return new Proxy({} as LocalActivityInterfaceFor<A>, {
get(_, activityType) {
if (typeof activityType !== 'string') {
throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`);
}
function localActivityProxyFunction(...args: unknown[]): Promise<unknown> {
return scheduleLocalActivity(activityType as string, args, options);
}
localActivityProxyFunction.executeWithOptions = function (
overrideOptions: LocalActivityOptions,
args: any[]
): Promise<unknown> {
return scheduleLocalActivity(activityType, args, deepMerge(options, overrideOptions));
};
return localActivityProxyFunction;
},
});
}
// TODO: deprecate this patch after "enough" time has passed
const EXTERNAL_WF_CANCEL_PATCH = '__temporal_internal_connect_external_handle_cancel_to_scope';
// The name of this patch comes from an attempt to build a generic internal patching mechanism.
// That effort has been abandoned in favor of a newer WorkflowTaskCompletedMetadata based mechanism.
const CONDITION_0_PATCH = '__sdk_internal_patch_number:1';
/**
* Returns a client-side handle that can be used to signal and cancel an existing Workflow execution.
* It takes a Workflow ID and optional run ID.
*/
export function getExternalWorkflowHandle(workflowId: string, runId?: string): ExternalWorkflowHandle {
const activator = assertInWorkflowContext(
'Workflow.getExternalWorkflowHandle(...) may only be used from a Workflow Execution. Consider using Client.workflow.getHandle(...) instead.)'
);
return {
workflowId,
runId,
cancel() {
return new Promise<void>((resolve, reject) => {
// Connect this cancel operation to the current cancellation scope.
// This is behavior was introduced after v0.22.0 and is incompatible
// with histories generated with previous SDK versions and thus requires
// patching.
//
// We try to delay patching as much as possible to avoid polluting
// histories unless strictly required.
const scope = CancellationScope.current();
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch((err) => {
if (patched(EXTERNAL_WF_CANCEL_PATCH)) {
reject(err);
}
})
);
}
if (scope.consideredCancelled) {
if (patched(EXTERNAL_WF_CANCEL_PATCH)) {
return;
}
}
const seq = activator.nextSeqs.cancelWorkflow++;
activator.pushCommand({
requestCancelExternalWorkflowExecution: {
seq,
workflowExecution: {
namespace: activator.info.namespace,
workflowId,
runId,
},
},
});
activator.completions.cancelWorkflow.set(seq, { resolve, reject });
});
},
signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
return composeInterceptors(
activator.interceptors.outbound,
'signalWorkflow',
signalWorkflowNextHandler
)({
seq: activator.nextSeqs.signalWorkflow++,
signalName: typeof def === 'string' ? def : def.name,
args,
target: {
type: 'external',
workflowExecution: { workflowId, runId },
},
headers: {},
});
},
};
}
/**
* Start a child Workflow execution
*
* - Returns a client-side handle that implements a child Workflow interface.
* - By default, a child will be scheduled on the same task queue as its parent.
*
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
* In order to query the child, use a {@link WorkflowClient} from an Activity.
*/
export async function startChild<T extends Workflow>(
workflowType: string,
options: WithWorkflowArgs<T, ChildWorkflowOptions>
): Promise<ChildWorkflowHandle<T>>;
/**
* Start a child Workflow execution
*
* - Returns a client-side handle that implements a child Workflow interface.
* - Deduces the Workflow type and signature from provided Workflow function.
* - By default, a child will be scheduled on the same task queue as its parent.
*
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
* In order to query the child, use a {@link WorkflowClient} from an Activity.
*/
export async function startChild<T extends Workflow>(
workflowFunc: T,
options: WithWorkflowArgs<T, ChildWorkflowOptions>
): Promise<ChildWorkflowHandle<T>>;
/**
* Start a child Workflow execution
*
* **Override for Workflows that accept no arguments**.
*
* - Returns a client-side handle that implements a child Workflow interface.
* - The child will be scheduled on the same task queue as its parent.
*
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
* In order to query the child, use a {@link WorkflowClient} from an Activity.
*/
export async function startChild<T extends () => Promise<any>>(workflowType: string): Promise<ChildWorkflowHandle<T>>;
/**
* Start a child Workflow execution
*
* **Override for Workflows that accept no arguments**.
*
* - Returns a client-side handle that implements a child Workflow interface.
* - Deduces the Workflow type and signature from provided Workflow function.
* - The child will be scheduled on the same task queue as its parent.
*
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
* In order to query the child, use a {@link WorkflowClient} from an Activity.
*/
export async function startChild<T extends () => Promise<any>>(workflowFunc: T): Promise<ChildWorkflowHandle<T>>;
export async function startChild<T extends Workflow>(
workflowTypeOrFunc: string | T,
options?: WithWorkflowArgs<T, ChildWorkflowOptions>
): Promise<ChildWorkflowHandle<T>> {
const activator = assertInWorkflowContext(
'Workflow.startChild(...) may only be used from a Workflow Execution. Consider using Client.workflow.start(...) instead.)'
);
const optionsWithDefaults = addDefaultWorkflowOptions(options ?? ({} as any));
const workflowType = extractWorkflowType(workflowTypeOrFunc);
const execute = composeInterceptors(
activator.interceptors.outbound,
'startChildWorkflowExecution',
startChildWorkflowExecutionNextHandler
);
const [started, completed] = await execute({
seq: activator.nextSeqs.childWorkflow++,
options: optionsWithDefaults,
headers: {},
workflowType,
});
const firstExecutionRunId = await started;
return {
workflowId: optionsWithDefaults.workflowId,
firstExecutionRunId,
async result(): Promise<WorkflowResultType<T>> {
return (await completed) as any;
},
async signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
return composeInterceptors(
activator.interceptors.outbound,
'signalWorkflow',
signalWorkflowNextHandler
)({
seq: activator.nextSeqs.signalWorkflow++,
signalName: typeof def === 'string' ? def : def.name,
args,
target: {
type: 'child',
childWorkflowId: optionsWithDefaults.workflowId,
},
headers: {},
});
},
};
}
/**
* Start a child Workflow execution and await its completion.
*
* - By default, a child will be scheduled on the same task queue as its parent.
* - This operation is cancellable using {@link CancellationScope}s.
*
* @return The result of the child Workflow.
*/
export async function executeChild<T extends Workflow>(
workflowType: string,
options: WithWorkflowArgs<T, ChildWorkflowOptions>
): Promise<WorkflowResultType<T>>;
/**
* Start a child Workflow execution and await its completion.
*
* - By default, a child will be scheduled on the same task queue as its parent.
* - Deduces the Workflow type and signature from provided Workflow function.
* - This operation is cancellable using {@link CancellationScope}s.
*
* @return The result of the child Workflow.
*/
export async function executeChild<T extends Workflow>(
workflowFunc: T,
options: WithWorkflowArgs<T, ChildWorkflowOptions>
): Promise<WorkflowResultType<T>>;
/**
* Start a child Workflow execution and await its completion.
*
* **Override for Workflows that accept no arguments**.
*
* - The child will be scheduled on the same task queue as its parent.
* - This operation is cancellable using {@link CancellationScope}s.
*
* @return The result of the child Workflow.
*/
export async function executeChild<T extends () => WorkflowReturnType>(
workflowType: string
): Promise<WorkflowResultType<T>>;
/**
* Start a child Workflow execution and await its completion.
*
* **Override for Workflows that accept no arguments**.
*
* - The child will be scheduled on the same task queue as its parent.
* - Deduces the Workflow type and signature from provided Workflow function.
* - This operation is cancellable using {@link CancellationScope}s.
*
* @return The result of the child Workflow.
*/
export async function executeChild<T extends () => WorkflowReturnType>(workflowFunc: T): Promise<WorkflowResultType<T>>;
export async function executeChild<T extends Workflow>(
workflowTypeOrFunc: string | T,
options?: WithWorkflowArgs<T, ChildWorkflowOptions>
): Promise<WorkflowResultType<T>> {
const activator = assertInWorkflowContext(
'Workflow.executeChild(...) may only be used from a Workflow Execution. Consider using Client.workflow.execute(...) instead.'
);
const optionsWithDefaults = addDefaultWorkflowOptions(options ?? ({} as any));
const workflowType = extractWorkflowType(workflowTypeOrFunc);
const execute = composeInterceptors(
activator.interceptors.outbound,
'startChildWorkflowExecution',
startChildWorkflowExecutionNextHandler
);
const execPromise = execute({
seq: activator.nextSeqs.childWorkflow++,
options: optionsWithDefaults,
headers: {},
workflowType,
});
untrackPromise(execPromise);
const completedPromise = execPromise.then(([_started, completed]) => completed);
untrackPromise(completedPromise);
return completedPromise as Promise<any>;
}
/**
* Get information about the current Workflow.
*
* WARNING: This function returns a frozen copy of WorkflowInfo, at the point where this method has been called.
* Changes happening at later point in workflow execution will not be reflected in the returned object.
*
* For this reason, we recommend calling `workflowInfo()` on every access to {@link WorkflowInfo}'s fields,
* rather than caching the `WorkflowInfo` object (or part of it) in a local variable. For example:
*
* ```ts
* // GOOD
* function myWorkflow() {
* doSomething(workflowInfo().searchAttributes)
* ...
* doSomethingElse(workflowInfo().searchAttributes)
* }
* ```
*
* vs
*
* ```ts
* // BAD
* function myWorkflow() {
* const attributes = workflowInfo().searchAttributes
* doSomething(attributes)
* ...
* doSomethingElse(attributes)
* }
* ```
*/
export function workflowInfo(): WorkflowInfo {
const activator = assertInWorkflowContext('Workflow.workflowInfo(...) may only be used from a Workflow Execution.');
return activator.info;
}
/**
* Get information about the current update if any.
*
* @return Info for the current update handler the code calling this is executing
* within if any.
*/
export function currentUpdateInfo(): UpdateInfo | undefined {
assertInWorkflowContext('Workflow.currentUpdateInfo(...) may only be used from a Workflow Execution.');
return UpdateScope.current();
}
/**
* Returns whether or not code is executing in workflow context
*/
export function inWorkflowContext(): boolean {
return maybeGetActivator() !== undefined;
}
/**
* Returns a function `f` that will cause the current Workflow to ContinueAsNew when called.
*
* `f` takes the same arguments as the Workflow function supplied to typeparam `F`.
*
* Once `f` is called, Workflow Execution immediately completes.
*/
export function makeContinueAsNewFunc<F extends Workflow>(
options?: ContinueAsNewOptions
): (...args: Parameters<F>) => Promise<never> {
const activator = assertInWorkflowContext(
'Workflow.continueAsNew(...) and Workflow.makeContinueAsNewFunc(...) may only be used from a Workflow Execution.'
);
const info = activator.info;
const { workflowType, taskQueue, ...rest } = options ?? {};
const requiredOptions = {
workflowType: workflowType ?? info.workflowType,
taskQueue: taskQueue ?? info.taskQueue,
...rest,
};
return (...args: Parameters<F>): Promise<never> => {
const fn = composeInterceptors(activator.interceptors.outbound, 'continueAsNew', async (input) => {
const { headers, args, options } = input;
throw new ContinueAsNew({
workflowType: options.workflowType,
arguments: toPayloads(activator.payloadConverter, ...args),
headers,
taskQueue: options.taskQueue,
memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo),
searchAttributes:
options.searchAttributes || options.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? encodeUnifiedSearchAttributes(options.searchAttributes, options.typedSearchAttributes) // eslint-disable-line deprecation/deprecation
: undefined,
workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
versioningIntent: versioningIntentToProto(options.versioningIntent), // eslint-disable-line deprecation/deprecation
});
});
return fn({
args,
headers: {},
options: requiredOptions,
});
};
}
/**
* {@link https://docs.temporal.io/concepts/what-is-continue-as-new/ | Continues-As-New} the current Workflow Execution
* with default options.
*
* Shorthand for `makeContinueAsNewFunc<F>()(...args)`. (See: {@link makeContinueAsNewFunc}.)
*
* @example
*
* ```ts
* import { continueAsNew } from '@temporalio/workflow';
* import { SearchAttributeType } from '@temporalio/common';
*
* export async function myWorkflow(n: number): Promise<void> {
* // ... Workflow logic
* await continueAsNew<typeof myWorkflow>(n + 1);
* }
* ```
*/
export function continueAsNew<F extends Workflow>(...args: Parameters<F>): Promise<never> {
return makeContinueAsNewFunc()(...args);
}
/**
* Generate an RFC compliant V4 uuid.
* Uses the workflow's deterministic PRNG making it safe for use within a workflow.
* This function is cryptographically insecure.
* See the {@link https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid | stackoverflow discussion}.
*/
export function uuid4(): string {
// Return the hexadecimal text representation of number `n`, padded with zeroes to be of length `p`
const ho = (n: number, p: number) => n.toString(16).padStart(p, '0');
// Create a view backed by a 16-byte buffer
const view = new DataView(new ArrayBuffer(16));
// Fill buffer with random values
view.setUint32(0, (Math.random() * 0x100000000) >>> 0);
view.setUint32(4, (Math.random() * 0x100000000) >>> 0);
view.setUint32(8, (Math.random() * 0x100000000) >>> 0);
view.setUint32(12, (Math.random() * 0x100000000) >>> 0);
// Patch the 6th byte to reflect a version 4 UUID
view.setUint8(6, (view.getUint8(6) & 0xf) | 0x40);
// Patch the 8th byte to reflect a variant 1 UUID (version 4 UUIDs are)
view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80);
// Compile the canonical textual form from the array data
return `${ho(view.getUint32(0), 8)}-${ho(view.getUint16(4), 4)}-${ho(view.getUint16(6), 4)}-${ho(
view.getUint16(8),
4
)}-${ho(view.getUint32(10), 8)}${ho(view.getUint16(14), 4)}`;
}
/**
* Patch or upgrade workflow code by checking or stating that this workflow has a certain patch.
*
* See {@link https://docs.temporal.io/typescript/versioning | docs page} for info.
*
* If the workflow is replaying an existing history, then this function returns true if that
* history was produced by a worker which also had a `patched` call with the same `patchId`.
* If the history was produced by a worker *without* such a call, then it will return false.
*
* If the workflow is not currently replaying, then this call *always* returns true.
*
* Your workflow code should run the "new" code if this returns true, if it returns false, you
* should run the "old" code. By doing this, you can maintain determinism.
*
* @param patchId An identifier that should be unique to this patch. It is OK to use multiple
* calls with the same ID, which means all such calls will always return the same value.
*/
export function patched(patchId: string): boolean {
const activator = assertInWorkflowContext(
'Workflow.patch(...) and Workflow.deprecatePatch may only be used from a Workflow Execution.'
);
return activator.patchInternal(patchId, false);
}
/**
* Indicate that a patch is being phased out.
*
* See {@link https://docs.temporal.io/typescript/versioning | docs page} for info.
*
* Workflows with this call may be deployed alongside workflows with a {@link patched} call, but
* they must *not* be deployed while any workers still exist running old code without a
* {@link patched} call, or any runs with histories produced by such workers exist. If either kind
* of worker encounters a history produced by the other, their behavior is undefined.
*
* Once all live workflow runs have been produced by workers with this call, you can deploy workers
* which are free of either kind of patch call for this ID. Workers with and without this call
* may coexist, as long as they are both running the "new" code.
*
* @param patchId An identifier that should be unique to this patch. It is OK to use multiple
* calls with the same ID, which means all such calls will always return the same value.
*/
export function deprecatePatch(patchId: string): void {
const activator = assertInWorkflowContext(
'Workflow.patch(...) and Workflow.deprecatePatch may only be used from a Workflow Execution.'
);
activator.patchInternal(patchId, true);
}
/**
* Returns a Promise that resolves when `fn` evaluates to `true` or `timeout` expires, providing
* options to configure the timer (i.e. provide metadata)
*
* @param timeout number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
*
* @returns a boolean indicating whether the condition was true before the timeout expires
*
* @experimental TimerOptions is a new addition and subject to change
*/
export function condition(fn: () => boolean, timeout: Duration, options: TimerOptions): Promise<boolean>;
/**
* Returns a Promise that resolves when `fn` evaluates to `true` or `timeout` expires.
*
* @param timeout number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
*
* @returns a boolean indicating whether the condition was true before the timeout expires
*/
export function condition(fn: () => boolean, timeout: Duration): Promise<boolean>;
/**
* Returns a Promise that resolves when `fn` evaluates to `true`.
*/
export function condition(fn: () => boolean): Promise<void>;
export async function condition(fn: () => boolean, timeout?: Duration, opts?: TimerOptions): Promise<void | boolean> {
assertInWorkflowContext('Workflow.condition(...) may only be used from a Workflow Execution.');
// Prior to 1.5.0, `condition(fn, 0)` was treated as equivalent to `condition(fn, undefined)`
if (timeout === 0 && !patched(CONDITION_0_PATCH)) {
return conditionInner(fn);
}
if (typeof timeout === 'number' || typeof timeout === 'string') {
return CancellationScope.cancellable(async () => {
try {
return await Promise.race([sleep(timeout, opts).then(() => false), conditionInner(fn).then(() => true)]);
} finally {
CancellationScope.current().cancel();
}
});
}
return conditionInner(fn);
}
function conditionInner(fn: () => boolean): Promise<void> {
const activator = getActivator();
return new Promise((resolve, reject) => {
const scope = CancellationScope.current();
if (scope.consideredCancelled) {
untrackPromise(scope.cancelRequested.catch(reject));
return;
}
const seq = activator.nextSeqs.condition++;
if (scope.cancellable) {
untrackPromise(
scope.cancelRequested.catch((err) => {
activator.blockedConditions.delete(seq);
reject(err);
})
);
}
// Eager evaluation
if (fn()) {
resolve();
return;
}
activator.blockedConditions.set(seq, { fn, resolve });
});
}
/**
* Define an update method for a Workflow.
*
* A definition is used to register a handler in the Workflow via {@link setHandler} and to update a Workflow using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
* A definition can be reused in multiple Workflows.
*/
export function defineUpdate<Ret, Args extends any[] = [], Name extends string = string>(
name: Name
): UpdateDefinition<Ret, Args, Name> {
return {
type: 'update',
name,
} as UpdateDefinition<Ret, Args, Name>;
}
/**
* Define a signal method for a Workflow.
*
* A definition is used to register a handler in the Workflow via {@link setHandler} and to signal a Workflow using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
* A definition can be reused in multiple Workflows.
*/
export function defineSignal<Args extends any[] = [], Name extends string = string>(
name: Name
): SignalDefinition<Args, Name> {
return {
type: 'signal',
name,
} as SignalDefinition<Args, Name>;
}
/**
* Define a query method for a Workflow.
*
* A definition is used to register a handler in the Workflow via {@link setHandler} and to query a Workflow using a {@link WorkflowHandle}.
* A definition can be reused in multiple Workflows.
*/
export function defineQuery<Ret, Args extends any[] = [], Name extends string = string>(
name: Name
): QueryDefinition<Ret, Args, Name> {
return {
type: 'query',
name,
} as QueryDefinition<Ret, Args, Name>;
}
/**
* Set a handler function for a Workflow update, signal, or query.
*
* If this function is called multiple times for a given update, signal, or query name the last handler will overwrite any previous calls.
*
* @param def an {@link UpdateDefinition}, {@link SignalDefinition}, or {@link QueryDefinition} as returned by {@link defineUpdate}, {@link defineSignal}, or {@link defineQuery} respectively.
* @param handler a compatible handler function for the given definition or `undefined` to unset the handler.
* @param options an optional `description` of the handler and an optional update `validator` function.
*/
export function setHandler<Ret, Args extends any[], T extends QueryDefinition<Ret, Args>>(
def: T,
handler: Handler<Ret, Args, T> | undefined,
options?: QueryHandlerOptions
): void;
export function setHandler<Ret, Args extends any[], T extends SignalDefinition<Args>>(
def: T,
handler: Handler<Ret, Args, T> | undefined,
options?: SignalHandlerOptions
): void;
export function setHandler<Ret, Args extends any[], T extends UpdateDefinition<Ret, Args>>(
def: T,
handler: Handler<Ret, Args, T> | undefined,
options?: UpdateHandlerOptions<Args>
): void;
// For Updates and Signals we want to make a public guarantee something like the
// following:
//
// "If a WFT contains a Signal/Update, and if a handler is available for that
// Signal/Update, then the handler will be executed.""
//
// However, that statement is not well-defined, leaving several questions open:
//
// 1. What does it mean for a handler to be "available"? What happens if the
// handler is not present initially but is set at some point during the
// Workflow code that is executed in that WFT? What happens if the handler is
// set and then deleted, or replaced with a different handler?
//
// 2. When is the handler executed? (When it first becomes available? At the end
// of the activation?) What are the execution semantics of Workflow and
// Signal/Update handler code given that they are concurrent? Can the user
// rely on Signal/Update side effects being reflected in the Workflow return
// value, or in the value passed to Continue-As-New? If the handler is an
// async function / coroutine, how much of it is executed and when is the
// rest executed?
//
// 3. What happens if the handler is not executed? (i.e. because it wasn't
// available in the sense defined by (1))
//
// 4. In the case of Update, when is the validation function executed?
//
// The implementation for Typescript is as follows:
//
// 1. sdk-core sorts Signal and Update jobs (and Patches) ahead of all other
// jobs. Thus if the handler is available at the start of the Activation then
// the Signal/Update will be executed before Workflow code is executed. If it
// is not, then the Signal/Update calls are pushed to a buffer.
//
// 2. On each call to setHandler for a given Signal/Update, we make a pass
// through the buffer list. If a buffered job is associated with the just-set
// handler, then the job is removed from the buffer and the initial
// synchronous portion of the handler is invoked on that input (i.e.
// preempting workflow code).
//
// Thus in the case of Typescript the questions above are answered as follows:
//
// 1. A handler is "available" if it is set at the start of the Activation or
// becomes set at any point during the Activation. If the handler is not set
// initially then it is executed as soon as it is set. Subsequent deletion or
// replacement by a different handler has no impact because the jobs it was
// handling have already been handled and are no longer in the buffer.
//
// 2. The handler is executed as soon as it becomes available. I.e. if the
// handler is set at the start of the Activation then it is executed when
// first attempting to process the Signal/Update job; alternatively, if it is
// set by a setHandler call made by Workflow code, then it is executed as
// part of that call (preempting Workflow code). Therefore, a user can rely
// on Signal/Update side effects being reflected in e.g. the Workflow return
// value, and in the value passed to Continue-As-New. Activation jobs are
// processed in the order supplied by sdk-core, i.e. Signals, then Updates,
// then other jobs. Within each group, the order sent by the server is
// preserved. If the handler is async, it is executed up to its first yield
// point.
//
// 3. Signal case: If a handler does not become available for a Signal job then
// the job remains in the buffer. If a handler for the Signal becomes
// available in a subsequent Activation (of the same or a subsequent WFT)
// then the handler will be executed. If not, then the Signal will never be
// responded to and this causes no error.
//
// Update case: If a handler does not become available for an Update job then
// the Update is rejected at the end of the Activation. Thus, if a user does
// not want an Update to be rejected for this re