@temporalio/workflow
Version:
Temporal.io SDK Workflow sub-package
1,112 lines • 48.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.workflowMetadataQuery = exports.enhancedStackTraceQuery = exports.stackTraceQuery = exports.NotAnActivityMethod = void 0;
exports.addDefaultWorkflowOptions = addDefaultWorkflowOptions;
exports.sleep = sleep;
exports.scheduleActivity = scheduleActivity;
exports.scheduleLocalActivity = scheduleLocalActivity;
exports.proxyActivities = proxyActivities;
exports.proxyLocalActivities = proxyLocalActivities;
exports.getExternalWorkflowHandle = getExternalWorkflowHandle;
exports.startChild = startChild;
exports.executeChild = executeChild;
exports.workflowInfo = workflowInfo;
exports.currentUpdateInfo = currentUpdateInfo;
exports.inWorkflowContext = inWorkflowContext;
exports.makeContinueAsNewFunc = makeContinueAsNewFunc;
exports.continueAsNew = continueAsNew;
exports.uuid4 = uuid4;
exports.patched = patched;
exports.deprecatePatch = deprecatePatch;
exports.condition = condition;
exports.defineUpdate = defineUpdate;
exports.defineSignal = defineSignal;
exports.defineQuery = defineQuery;
exports.setHandler = setHandler;
exports.setDefaultSignalHandler = setDefaultSignalHandler;
exports.upsertSearchAttributes = upsertSearchAttributes;
exports.upsertMemo = upsertMemo;
exports.allHandlersFinished = allHandlersFinished;
const common_1 = require("@temporalio/common");
const versioning_intent_enum_1 = require("@temporalio/common/lib/versioning-intent-enum");
const time_1 = require("@temporalio/common/lib/time");
const interceptors_1 = require("@temporalio/common/lib/interceptors");
const cancellation_scope_1 = require("./cancellation-scope");
const update_scope_1 = require("./update-scope");
const interfaces_1 = require("./interfaces");
const errors_1 = require("./errors");
const global_attributes_1 = require("./global-attributes");
const stack_helpers_1 = require("./stack-helpers");
// Avoid a circular dependency
(0, cancellation_scope_1.registerSleepImplementation)(sleep);
/**
* Adds default values of `workflowId` and `cancellationType` to given workflow options.
*/
function addDefaultWorkflowOptions(opts) {
const { args, workflowId, ...rest } = opts;
return {
workflowId: workflowId ?? uuid4(),
args: (args ?? []),
cancellationType: interfaces_1.ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
...rest,
};
}
/**
* Push a startTimer command into state accumulator and register completion
*/
function timerNextHandler(input) {
const activator = (0, global_attributes_1.getActivator)();
return new Promise((resolve, reject) => {
const scope = cancellation_scope_1.CancellationScope.current();
if (scope.consideredCancelled) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch((err) => {
if (!activator.completions.timer.delete(input.seq)) {
return; // Already resolved or never scheduled
}
activator.pushCommand({
cancelTimer: {
seq: input.seq,
},
});
reject(err);
}));
}
activator.pushCommand({
startTimer: {
seq: input.seq,
startToFireTimeout: (0, time_1.msToTs)(input.durationMs),
},
});
activator.completions.timer.set(input.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.
*/
function sleep(ms) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.sleep(...) may only be used from a Workflow Execution');
const seq = activator.nextSeqs.timer++;
const durationMs = Math.max(1, (0, time_1.msToNumber)(ms));
const execute = (0, interceptors_1.composeInterceptors)(activator.interceptors.outbound, 'startTimer', timerNextHandler);
return execute({
durationMs,
seq,
});
}
function validateActivityOptions(options) {
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 }) {
const activator = (0, global_attributes_1.getActivator)();
validateActivityOptions(options);
return new Promise((resolve, reject) => {
const scope = cancellation_scope_1.CancellationScope.current();
if (scope.consideredCancelled) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
(0, stack_helpers_1.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: (0, common_1.toPayloads)(activator.payloadConverter, ...args),
retryPolicy: options.retry ? (0, common_1.compileRetryPolicy)(options.retry) : undefined,
taskQueue: options.taskQueue || activator.info.taskQueue,
heartbeatTimeout: (0, time_1.msOptionalToTs)(options.heartbeatTimeout),
scheduleToCloseTimeout: (0, time_1.msOptionalToTs)(options.scheduleToCloseTimeout),
startToCloseTimeout: (0, time_1.msOptionalToTs)(options.startToCloseTimeout),
scheduleToStartTimeout: (0, time_1.msOptionalToTs)(options.scheduleToStartTimeout),
headers,
cancellationType: (0, common_1.encodeActivityCancellationType)(options.cancellationType),
doNotEagerlyExecute: !(options.allowEagerDispatch ?? true),
versioningIntent: (0, versioning_intent_enum_1.versioningIntentToProto)(options.versioningIntent),
},
});
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, }) {
const activator = (0, global_attributes_1.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 = cancellation_scope_1.CancellationScope.current();
if (scope.consideredCancelled) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
(0, stack_helpers_1.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: (0, common_1.toPayloads)(activator.payloadConverter, ...args),
retryPolicy: options.retry ? (0, common_1.compileRetryPolicy)(options.retry) : undefined,
scheduleToCloseTimeout: (0, time_1.msOptionalToTs)(options.scheduleToCloseTimeout),
startToCloseTimeout: (0, time_1.msOptionalToTs)(options.startToCloseTimeout),
scheduleToStartTimeout: (0, time_1.msOptionalToTs)(options.scheduleToStartTimeout),
localRetryThreshold: (0, time_1.msOptionalToTs)(options.localRetryThreshold),
headers,
cancellationType: (0, common_1.encodeActivityCancellationType)(options.cancellationType),
},
});
activator.completions.activity.set(seq, {
resolve,
reject,
});
});
}
/**
* Schedule an activity and run outbound interceptors
* @hidden
*/
function scheduleActivity(activityType, args, options) {
const activator = (0, global_attributes_1.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 = (0, interceptors_1.composeInterceptors)(activator.interceptors.outbound, 'scheduleActivity', scheduleActivityNextHandler);
return execute({
activityType,
headers: {},
options,
args,
seq,
});
}
/**
* Schedule an activity and run outbound interceptors
* @hidden
*/
async function scheduleLocalActivity(activityType, args, options) {
const activator = (0, global_attributes_1.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 = (0, interceptors_1.composeInterceptors)(activator.interceptors.outbound, 'scheduleLocalActivity', scheduleLocalActivityNextHandler);
try {
return (await execute({
activityType,
headers: {},
options,
args,
seq,
attempt,
originalScheduleTime,
}));
}
catch (err) {
if (err instanceof errors_1.LocalActivityDoBackoff) {
await sleep((0, time_1.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, }) {
const activator = (0, global_attributes_1.getActivator)();
const workflowId = options.workflowId ?? uuid4();
const startPromise = new Promise((resolve, reject) => {
const scope = cancellation_scope_1.CancellationScope.current();
if (scope.consideredCancelled) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
(0, stack_helpers_1.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: (0, common_1.toPayloads)(activator.payloadConverter, ...options.args),
retryPolicy: options.retry ? (0, common_1.compileRetryPolicy)(options.retry) : undefined,
taskQueue: options.taskQueue || activator.info.taskQueue,
workflowExecutionTimeout: (0, time_1.msOptionalToTs)(options.workflowExecutionTimeout),
workflowRunTimeout: (0, time_1.msOptionalToTs)(options.workflowRunTimeout),
workflowTaskTimeout: (0, time_1.msOptionalToTs)(options.workflowTaskTimeout),
namespace: activator.info.namespace, // Not configurable
headers,
cancellationType: (0, interfaces_1.encodeChildWorkflowCancellationType)(options.cancellationType),
workflowIdReusePolicy: (0, common_1.encodeWorkflowIdReusePolicy)(options.workflowIdReusePolicy),
parentClosePolicy: (0, interfaces_1.encodeParentClosePolicy)(options.parentClosePolicy),
cronSchedule: options.cronSchedule,
searchAttributes: options.searchAttributes
? (0, common_1.mapToPayloads)(common_1.searchAttributePayloadConverter, options.searchAttributes)
: undefined,
memo: options.memo && (0, common_1.mapToPayloads)(activator.payloadConverter, options.memo),
versioningIntent: (0, versioning_intent_enum_1.versioningIntentToProto)(options.versioningIntent),
},
});
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.
(0, stack_helpers_1.untrackPromise)(startPromise.catch(reject));
activator.completions.childWorkflowComplete.set(seq, {
resolve,
reject,
});
});
(0, stack_helpers_1.untrackPromise)(startPromise);
(0, stack_helpers_1.untrackPromise)(completePromise);
// Prevent unhandled rejection because the completion might not be awaited
(0, stack_helpers_1.untrackPromise)(completePromise.catch(() => undefined));
const ret = new Promise((resolve) => resolve([startPromise, completePromise]));
(0, stack_helpers_1.untrackPromise)(ret);
return ret;
}
function signalWorkflowNextHandler({ seq, signalName, args, target, headers }) {
const activator = (0, global_attributes_1.getActivator)();
return new Promise((resolve, reject) => {
const scope = cancellation_scope_1.CancellationScope.current();
if (scope.consideredCancelled) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(reject));
return;
}
if (scope.cancellable) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(() => {
if (!activator.completions.signalWorkflow.has(seq)) {
return;
}
activator.pushCommand({ cancelSignalWorkflow: { seq } });
}));
}
activator.pushCommand({
signalExternalWorkflowExecution: {
seq,
args: (0, common_1.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}
*/
exports.NotAnActivityMethod = Symbol.for('__TEMPORAL_NOT_AN_ACTIVITY_METHOD');
/**
* 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',
* });
*
* // 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");
* // ...
* }
* ```
*/
function proxyActivities(options) {
if (options === undefined) {
throw new TypeError('options must be defined');
}
// Validate as early as possible for immediate user feedback
validateActivityOptions(options);
return new Proxy({}, {
get(_, activityType) {
if (typeof activityType !== 'string') {
throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`);
}
return function activityProxyFunction(...args) {
return scheduleActivity(activityType, args, options);
};
},
});
}
/**
* 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
*/
function proxyLocalActivities(options) {
if (options === undefined) {
throw new TypeError('options must be defined');
}
// Validate as early as possible for immediate user feedback
validateLocalActivityOptions(options);
return new Proxy({}, {
get(_, activityType) {
if (typeof activityType !== 'string') {
throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`);
}
return function localActivityProxyFunction(...args) {
return scheduleLocalActivity(activityType, args, options);
};
},
});
}
// 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.
*/
function getExternalWorkflowHandle(workflowId, runId) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.getExternalWorkflowHandle(...) may only be used from a Workflow Execution. Consider using Client.workflow.getHandle(...) instead.)');
return {
workflowId,
runId,
cancel() {
return new Promise((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 = cancellation_scope_1.CancellationScope.current();
if (scope.cancellable) {
(0, stack_helpers_1.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(def, ...args) {
return (0, interceptors_1.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: {},
});
},
};
}
async function startChild(workflowTypeOrFunc, options) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.startChild(...) may only be used from a Workflow Execution. Consider using Client.workflow.start(...) instead.)');
const optionsWithDefaults = addDefaultWorkflowOptions(options ?? {});
const workflowType = (0, common_1.extractWorkflowType)(workflowTypeOrFunc);
const execute = (0, interceptors_1.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() {
return (await completed);
},
async signal(def, ...args) {
return (0, interceptors_1.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: {},
});
},
};
}
async function executeChild(workflowTypeOrFunc, options) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.executeChild(...) may only be used from a Workflow Execution. Consider using Client.workflow.execute(...) instead.');
const optionsWithDefaults = addDefaultWorkflowOptions(options ?? {});
const workflowType = (0, common_1.extractWorkflowType)(workflowTypeOrFunc);
const execute = (0, interceptors_1.composeInterceptors)(activator.interceptors.outbound, 'startChildWorkflowExecution', startChildWorkflowExecutionNextHandler);
const execPromise = execute({
seq: activator.nextSeqs.childWorkflow++,
options: optionsWithDefaults,
headers: {},
workflowType,
});
(0, stack_helpers_1.untrackPromise)(execPromise);
const completedPromise = execPromise.then(([_started, completed]) => completed);
(0, stack_helpers_1.untrackPromise)(completedPromise);
return completedPromise;
}
/**
* 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)
* }
* ```
*/
function workflowInfo() {
const activator = (0, global_attributes_1.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.
*/
function currentUpdateInfo() {
(0, global_attributes_1.assertInWorkflowContext)('Workflow.currentUpdateInfo(...) may only be used from a Workflow Execution.');
return update_scope_1.UpdateScope.current();
}
/**
* Returns whether or not code is executing in workflow context
*/
function inWorkflowContext() {
return (0, global_attributes_1.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.
*/
function makeContinueAsNewFunc(options) {
const activator = (0, global_attributes_1.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) => {
const fn = (0, interceptors_1.composeInterceptors)(activator.interceptors.outbound, 'continueAsNew', async (input) => {
const { headers, args, options } = input;
throw new interfaces_1.ContinueAsNew({
workflowType: options.workflowType,
arguments: (0, common_1.toPayloads)(activator.payloadConverter, ...args),
headers,
taskQueue: options.taskQueue,
memo: options.memo && (0, common_1.mapToPayloads)(activator.payloadConverter, options.memo),
searchAttributes: options.searchAttributes
? (0, common_1.mapToPayloads)(common_1.searchAttributePayloadConverter, options.searchAttributes)
: undefined,
workflowRunTimeout: (0, time_1.msOptionalToTs)(options.workflowRunTimeout),
workflowTaskTimeout: (0, time_1.msOptionalToTs)(options.workflowTaskTimeout),
versioningIntent: (0, versioning_intent_enum_1.versioningIntentToProto)(options.versioningIntent),
});
});
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';
*
*export async function myWorkflow(n: number): Promise<void> {
* // ... Workflow logic
* await continueAsNew<typeof myWorkflow>(n + 1);
*}
*```
*/
function continueAsNew(...args) {
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}.
*/
function uuid4() {
// Return the hexadecimal text representation of number `n`, padded with zeroes to be of length `p`
const ho = (n, p) => 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.
*/
function patched(patchId) {
const activator = (0, global_attributes_1.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.
*/
function deprecatePatch(patchId) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.patch(...) and Workflow.deprecatePatch may only be used from a Workflow Execution.');
activator.patchInternal(patchId, true);
}
async function condition(fn, timeout) {
(0, global_attributes_1.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 cancellation_scope_1.CancellationScope.cancellable(async () => {
try {
return await Promise.race([sleep(timeout).then(() => false), conditionInner(fn).then(() => true)]);
}
finally {
cancellation_scope_1.CancellationScope.current().cancel();
}
});
}
return conditionInner(fn);
}
function conditionInner(fn) {
const activator = (0, global_attributes_1.getActivator)();
return new Promise((resolve, reject) => {
const scope = cancellation_scope_1.CancellationScope.current();
if (scope.consideredCancelled) {
(0, stack_helpers_1.untrackPromise)(scope.cancelRequested.catch(reject));
return;
}
const seq = activator.nextSeqs.condition++;
if (scope.cancellable) {
(0, stack_helpers_1.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.
*/
function defineUpdate(name) {
return {
type: 'update',
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.
*/
function defineSignal(name) {
return {
type: 'signal',
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.
*/
function defineQuery(name) {
return {
type: 'query',
name,
};
}
// 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 reason, then it is their
// responsibility to ensure that their application and workflow code interact
// such that a handler is available for the Update during any Activation
// which might contain their Update job. (Note that the user often has
// uncertainty about which WFT their Signal/Update will appear in. For
// example, if they call startWorkflow() followed by startUpdate(), then they
// will typically not know whether these will be delivered in one or two
// WFTs. On the other hand there are situations where they would have reason
// to believe they are in the same WFT, for example if they do not start
// Worker polling until after they have verified that both requests have
// succeeded.)
//
// 4. If an Update has a validation function then it is executed immediately
// prior to the handler. (Note that the validation function is required to be
// synchronous).
function setHandler(def, handler, options) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.setHandler(...) may only be used from a Workflow Execution.');
const description = options?.description;
if (def.type === 'update') {
if (typeof handler === 'function') {
const updateOptions = options;
const validator = updateOptions?.validator;
const unfinishedPolicy = updateOptions?.unfinishedPolicy ?? common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON;
activator.updateHandlers.set(def.name, { handler, validator, description, unfinishedPolicy });
activator.dispatchBufferedUpdates();
}
else if (handler == null) {
activator.updateHandlers.delete(def.name);
}
else {
throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
}
}
else if (def.type === 'signal') {
if (typeof handler === 'function') {
const signalOptions = options;
const unfinishedPolicy = signalOptions?.unfinishedPolicy ?? common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON;
activator.signalHandlers.set(def.name, { handler: handler, description, unfinishedPolicy });
activator.dispatchBufferedSignals();
}
else if (handler == null) {
activator.signalHandlers.delete(def.name);
}
else {
throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
}
}
else if (def.type === 'query') {
if (typeof handler === 'function') {
activator.queryHandlers.set(def.name, { handler: handler, description });
}
else if (handler == null) {
activator.queryHandlers.delete(def.name);
}
else {
throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
}
}
else {
throw new TypeError(`Invalid definition type: ${def.type}`);
}
}
/**
* Set a signal handler function that will handle signals calls for non-registered signal names.
*
* Signals are dispatched to the default signal handler in the order that they were accepted by the server.
*
* If this function is called multiple times for a given signal or query name the last handler will overwrite any previous calls.
*
* @param handler a function that will handle signals for non-registered signal names, or `undefined` to unset the handler.
*/
function setDefaultSignalHandler(handler) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.setDefaultSignalHandler(...) may only be used from a Workflow Execution.');
if (typeof handler === 'function') {
activator.defaultSignalHandler = handler;
activator.dispatchBufferedSignals();
}
else if (handler == null) {
activator.defaultSignalHandler = undefined;
}
else {
throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
}
}
/**
* Updates this Workflow's Search Attributes by merging the provided `searchAttributes` with the existing Search
* Attributes, `workflowInfo().searchAttributes`.
*
* For example, this Workflow code:
*
* ```ts
* upsertSearchAttributes({
* CustomIntField: [1],
* CustomBoolField: [true]
* });
* upsertSearchAttributes({
* CustomIntField: [42],
* CustomKeywordField: ['durable code', 'is great']
* });
* ```
*
* would result in the Workflow having these Search Attributes:
*
* ```ts
* {
* CustomIntField: [42],
* CustomBoolField: [true],
* CustomKeywordField: ['durable code', 'is great']
* }
* ```
*
* @param searchAttributes The Record to merge. Use a value of `[]` to clear a Search Attribute.
*/
function upsertSearchAttributes(searchAttributes) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.upsertSearchAttributes(...) may only be used from a Workflow Execution.');
if (searchAttributes == null) {
throw new Error('searchAttributes must be a non-null SearchAttributes');
}
activator.pushCommand({
upsertWorkflowSearchAttributes: {
searchAttributes: (0, common_1.mapToPayloads)(common_1.searchAttributePayloadConverter, searchAttributes),
},
});
activator.mutateWorkflowInfo((info) => {
return {
...info,
searchAttributes: {
...info.searchAttributes,
...searchAttributes,
},
};
});
}
/**
* Updates this Workflow's Memos by merging the provided `memo` with existing
* Memos (as returned by `workflowInfo().memo`).
*
* New memo is merged by replacing properties of the same name _at the first
* level only_. Setting a property to value `undefined` or `null` clears that
* key from the Memo.
*
* For example:
*
* ```ts
* upsertMemo({
* key1: value,
* key3: { subkey1: value }
* key4: value,
* });
* upsertMemo({
* key2: value
* key3: { subkey2: value }
* key4: undefined,
* });
* ```
*
* would result in the Workflow having these Memo:
*
* ```ts
* {
* key1: value,
* key2: value,
* key3: { subkey2: value } // Note this object was completely replaced
* // Note that key4 was completely removed
* }
* ```
*
* @param memo The Record to merge.
*/
function upsertMemo(memo) {
const activator = (0, global_attributes_1.assertInWorkflowContext)('Workflow.upsertMemo(...) may only be used from a Workflow Execution.');
if (memo == null) {
throw new Error('memo must be a non-null Record');
}
activator.pushCommand({
modifyWorkflowProperties: {
upsertedMemo: {
fields: (0, common_1.mapToPayloads)(activator.payloadConverter,
// Convert null to undefined
Object.fromEntries(Object.entries(memo).map(([k, v]) => [k, v ?? undefined]))),
},
},
});
activator.mutateWorkflowInfo((info) => {
return {
...info,
memo: Object.fromEntries(Object.entries({
...info.memo,
...memo,
}).filter(([_, v]) => v != null)),
};
});
}
/**
* Whether update and signal handlers have finished executing.
*
* Consider waiting on this condition before workflow return or continue-as-new, to prevent
* interruption of in-progress handlers by workflow exit:
*
* ```ts
* await workflow.condition(workflow.allHandlersFinished)
* ```
*
* @returns true if there are no in-progress update or signal handler executions.
*/
function allHandlersFinished() {
const activator = (0, global_attributes_1.assertInWorkflowContext)('allHandlersFinished() may only be used from a Workflow Execution.');
return activator.inProgressSignals.size === 0 && activator.inProgressUpdates.size === 0;
}
exports.stackTraceQuery = defineQuery('__stack_trace');
exports.enhancedStackTraceQuery = defineQuery('__enhanced_stack_trace');
exports.workflowMetadataQuery = defineQuery('__temporal_workflow_metadata');
//# sourceMappingURL=workflow.js.map
;