@temporalio/workflow
Version:
Temporal.io SDK Workflow sub-package
1,095 lines • 54.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Activator = void 0;
const common_1 = require("@temporalio/common");
const payload_search_attributes_1 = require("@temporalio/common/lib/converter/payload-search-attributes");
const internal_workflow_1 = require("@temporalio/common/lib/internal-workflow");
const reserved_1 = require("@temporalio/common/lib/reserved");
const alea_1 = require("./alea");
const cancellation_scope_1 = require("./cancellation-scope");
const interceptor_composition_1 = require("./interceptor-composition");
const update_scope_1 = require("./update-scope");
const random_stream_seed_1 = require("./random-stream-seed");
const errors_1 = require("./errors");
const interfaces_1 = require("./interfaces");
const stack_helpers_1 = require("./stack-helpers");
const pkg_1 = __importDefault(require("./pkg"));
const flags_1 = require("./flags");
const logs_1 = require("./logs");
const StartChildWorkflowExecutionFailedCause = {
WORKFLOW_ALREADY_EXISTS: 'WORKFLOW_ALREADY_EXISTS',
};
const [_encodeStartChildWorkflowExecutionFailedCause, decodeStartChildWorkflowExecutionFailedCause] = (0, internal_workflow_1.makeProtoEnumConverters)({
[StartChildWorkflowExecutionFailedCause.WORKFLOW_ALREADY_EXISTS]: 1,
UNSPECIFIED: 0,
}, 'START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_');
/**
* Keeps all of the Workflow runtime state like pending completions for activities and timers.
*
* Implements handlers for all workflow activation jobs.
*
* Note that most methods in this class are meant to be called only from within the VM.
*
* However, a few methods may be called directly from outside the VM (essentially from `vm-shared.ts`).
* These methods are specifically marked with a comment and require careful consideration, as the
* execution context may not properly reflect that of the target workflow execution (e.g.: with Reusable
* VMs, the `global` may not have been swapped to those of that workflow execution; the active microtask
* queue may be that of the thread/process, rather than the queue of that VM context; etc). Consequently,
* methods that are meant to be called from outside of the VM must not do any of the following:
*
* - Access any global variable;
* - Create Promise objects, use async/await, or otherwise schedule microtasks;
* - Call user-defined functions, including any form of interceptor.
*/
class Activator {
/**
* Cache for modules - referenced in reusable-vm.ts
*/
moduleCache = new Map();
/**
* Map of task sequence to a Completion
*/
completions = {
timer: new Map(),
activity: new Map(),
nexusOperationStart: new Map(),
nexusOperationComplete: new Map(),
childWorkflowStart: new Map(),
childWorkflowComplete: new Map(),
signalWorkflow: new Map(),
cancelWorkflow: new Map(),
};
/**
* Holds buffered Update calls until a handler is registered
*/
bufferedUpdates = Array();
/**
* Holds buffered signal calls until a handler is registered
*/
bufferedSignals = Array();
/**
* Mapping of update name to handler and validator
*/
updateHandlers = new Map();
/**
* Mapping of signal name to handler
*/
signalHandlers = new Map();
/**
* Mapping of in-progress updates to handler execution information.
*/
inProgressUpdates = new Map();
/**
* Mapping of in-progress signals to handler execution information.
*/
inProgressSignals = new Map();
/**
* A sequence number providing unique identifiers for signal handler executions.
*/
signalHandlerExecutionSeq = 0;
/**
* A signal handler that catches calls for non-registered signal names.
*/
defaultSignalHandler;
/**
* A update handler that catches calls for non-registered update names.
*/
defaultUpdateHandler;
/**
* A query handler that catches calls for non-registered query names.
*/
defaultQueryHandler;
/**
* Source map file for looking up the source files in response to __enhanced_stack_trace
*/
sourceMap;
/**
* Whether or not to send the sources in enhanced stack trace query responses
*/
showStackTraceSources;
promiseStackStore = {
promiseToStack: new Map(),
childToParent: new Map(),
};
/**
* The error that caused the current Workflow Task to fail. Sets if a non-`TemporalFailure`
* error bubbles up out of the Workflow function, or out of a Signal or Update handler. We
* capture errors this way because those functions are not technically awaited when started,
* but left to run asynchronously. There is therefore no real "parent" function that can
* directly handle those errors, and not capturing it would result in an Unhandled Promise
* Rejection. So instead, we buffer the error here, to then be processed in the context
* of our own synchronous Activation handling event loop.
*
* Our code does a best effort to stop processing the current activation as soon as possible
* after this field is set:
* - If an error is thrown while executing code synchronously (e.g. anything before the
* first `await` statement in a Workflow function or a signal/update handler), the error
* will be _immediately_ rethrown, which will prevent execution of further jobs in the
* current activation. We know we're currently running code synchronously thanks to the
* `rethrowSynchronously` flag below.
* - It an error is thrown while executing microtasks, then the error will be rethrown on
* the next call to `tryUnblockConditions()`.
*
* Unfortunately, there's no way for us to prevent further execution of microtasks that have
* already been scheduled, nor those that will be recursively scheduled from those microtasks.
* Should more errors get thrown while settling microtasks, those will be ignored (i.e. only
* the first captured error is preserved).
*/
workflowTaskError;
/**
* Error type _names_ (from {@link WorkerOptions.workflowFailureErrorTypes}) that
* should cause Workflow Execution failure rather than WFT failure.
*
* Set at workflow creation time from the worker options.
*/
failureExceptionTypeNames = [];
/**
* Error _types_ (from {@link WorkflowDefinitionOptions.failureExceptionTypes})
* that should cause Workflow Execution failure rather than WFT failure.
*
* Set in `worker-interface.ts` after the workflow definition options are read.
*/
workflowDefinitionFailureExceptionTypes = undefined;
/**
* Set to true when running synchronous code (e.g. while processing activation jobs and when calling
* `tryUnblockConditions()`). While this flag is set, it is safe to let errors bubble up.
*/
rethrowSynchronously = false;
rootScope = new cancellation_scope_1.RootCancellationScope();
/**
* Mapping of query name to handler
*/
queryHandlers = new Map([
[
reserved_1.STACK_TRACE_QUERY_NAME,
{
handler: () => {
return new common_1.RawValue(this.getStackTraces()
.map((s) => s.formatted)
.join('\n\n'));
},
description: 'Returns a sensible stack trace.',
},
],
[
reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME,
{
handler: () => {
const { sourceMap } = this;
const sdk = { name: 'typescript', version: pkg_1.default.version };
const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
const sources = {};
if (this.showStackTraceSources) {
for (const { locations } of stacks) {
for (const { file_path } of locations) {
if (!file_path)
continue;
const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(file_path)];
if (!content)
continue;
sources[file_path] = [
{
line_offset: 0,
content,
},
];
}
}
}
return new common_1.RawValue({ sdk, stacks, sources });
},
description: 'Returns a stack trace annotated with source information.',
},
],
[
'__temporal_workflow_metadata',
{
handler: () => {
const workflowType = this.info.workflowType;
const queryDefinitions = Array.from(this.queryHandlers.entries()).map(([name, value]) => ({
name,
description: value.description,
}));
const signalDefinitions = Array.from(this.signalHandlers.entries()).map(([name, value]) => ({
name,
description: value.description,
}));
const updateDefinitions = Array.from(this.updateHandlers.entries()).map(([name, value]) => ({
name,
description: value.description,
}));
return new common_1.RawValue({
definition: {
type: workflowType,
queryDefinitions,
signalDefinitions,
updateDefinitions,
},
currentDetails: this.currentDetails,
});
},
description: 'Returns metadata associated with this workflow.',
},
],
]);
/**
* Loaded in {@link initRuntime}
*/
interceptors = {
inbound: [],
outbound: [],
internals: [],
};
/**
* Buffer that stores all generated commands, reset after each activation
*/
commands = [];
/**
* Stores all {@link condition}s that haven't been unblocked yet
*/
blockedConditions = new Map();
/**
* Is this Workflow completed?
*
* A Workflow will be considered completed if it generates a command that the
* system considers as a final Workflow command (e.g.
* completeWorkflowExecution or failWorkflowExecution).
*/
completed = false;
/**
* Was this Workflow cancelled?
*/
cancelled = false;
/**
* The next (incremental) sequence to assign when generating completable commands
*/
nextSeqs = {
timer: 1,
activity: 1,
childWorkflow: 1,
signalWorkflow: 1,
cancelWorkflow: 1,
condition: 1,
nexusOperation: 1,
// Used internally to keep track of active stack traces
stack: 1,
};
/**
* This is set every time the workflow executes an activation
* May be accessed and modified from outside the VM.
*/
now;
/**
* Reference to the current Workflow, initialized when a Workflow is started
*/
workflow;
/**
* Information about the current Workflow
* May be accessed from outside the VM.
*/
info;
/**
* The main deterministic RNG for this workflow execution.
*
* Scoped overrides used by `WorkflowRandomStream.with(...)` are layered on top of this RNG.
*/
random;
/**
* The current seed material for this workflow execution's deterministic RNGs.
*/
randomnessSeed;
/**
* Additional deterministic RNG streams keyed by stable stream name.
*/
namedRandomStreams = new Map();
currentRandomStorage;
payloadConverter = common_1.defaultPayloadConverter;
failureConverter = common_1.defaultFailureConverter;
/**
* Patches we know the status of for this workflow, as in {@link patched}
*/
knownPresentPatches = new Set();
/**
* Patches we sent to core {@link patched}
*/
sentPatches = new Set();
knownFlags = new Set();
sdkVersion;
/**
* Buffered sink calls per activation
*/
sinkCalls = Array();
/**
* A nanosecond resolution time function, externally injected. This is used to
* precisely sort logs entries emitted from the Workflow Context vs those emitted
* from other sources (e.g. main thread, Core, etc).
*/
getTimeOfDay;
registeredActivityNames;
currentDetails = '';
versioningBehavior;
workflowDefinitionOptionsGetter;
workflowSandboxDestructors = [];
stackTracesEnabled;
constructor({ info, now, showStackTraceSources, sourceMap, getTimeOfDay, randomnessSeed, registeredActivityNames, stackTracesEnabled, failureExceptionTypeNames, }) {
this.getTimeOfDay = getTimeOfDay;
this.info = info;
this.now = now;
this.showStackTraceSources = showStackTraceSources;
this.sourceMap = sourceMap;
this.randomnessSeed = [...randomnessSeed];
this.random = (0, alea_1.alea)(this.randomnessSeed);
this.registeredActivityNames = registeredActivityNames;
this.stackTracesEnabled = stackTracesEnabled;
this.failureExceptionTypeNames = failureExceptionTypeNames ?? [];
}
setRandomnessSeed(randomnessSeed) {
this.randomnessSeed = [...randomnessSeed];
this.random = (0, alea_1.alea)(this.randomnessSeed);
this.namedRandomStreams.clear();
}
getNamedRandom(name) {
const cached = this.namedRandomStreams.get(name);
if (cached !== undefined) {
return cached;
}
const random = (0, alea_1.alea)((0, random_stream_seed_1.deriveAleaSeed)(this.randomnessSeed, name));
this.namedRandomStreams.set(name, random);
return random;
}
withRandomSource(randomSource, fn) {
return (this.currentRandomStorage ??= new update_scope_1.AsyncLocalStorage()).run(randomSource, fn);
}
withCurrentRandom(randomSource, fn) {
return this.withRandomSource(randomSource, fn);
}
bindCurrentRandom(fn) {
const randomSource = this.currentRandomStorage?.getStore();
return ((...args) => this.withRandomSource(randomSource, () => fn(...args)));
}
currentRandom() {
return this.currentRandomStorage?.getStore()?.random() ?? this.random();
}
/**
* May be invoked from outside the VM.
*/
mutateWorkflowInfo(fn) {
this.info = fn(this.info);
}
getStackTraces() {
if (!this.stackTracesEnabled) {
throw new common_1.IllegalStateError('Workflow stack traces are not enabled on this worker');
}
const { childToParent, promiseToStack } = this.promiseStackStore;
const internalNodes = [...childToParent.values()].reduce((acc, curr) => {
for (const p of curr) {
acc.add(p);
}
return acc;
}, new Set());
const stacks = new Map();
for (const child of childToParent.keys()) {
if (!internalNodes.has(child)) {
const stack = promiseToStack.get(child);
if (!stack || !stack.formatted)
continue;
stacks.set(stack.formatted, stack);
}
}
// Not 100% sure where this comes from, just filter it out
stacks.delete(' at Promise.then (<anonymous>)');
stacks.delete(' at Promise.then (<anonymous>)\n');
return [...stacks].map(([_, stack]) => stack);
}
/**
* May be invoked from outside the VM.
*/
getAndResetSinkCalls() {
const { sinkCalls } = this;
this.sinkCalls = [];
return sinkCalls;
}
/**
* Buffer a Workflow command to be collected at the end of the current activation.
*
* Prevents commands from being added after Workflow completion.
*/
pushCommand(cmd, complete = false) {
this.commands.push(cmd);
if (complete) {
this.completed = true;
}
}
concludeActivation() {
return {
commands: this.commands.splice(0),
usedInternalFlags: [...this.knownFlags],
versioningBehavior: this.versioningBehavior,
};
}
async startWorkflowNextHandler({ args }) {
const { workflow } = this;
if (workflow == null) {
throw new common_1.IllegalStateError('Workflow uninitialized');
}
return await workflow(...args);
}
startWorkflow(activation) {
const execute = (0, interceptor_composition_1.composeInterceptors)(this.interceptors.inbound, 'execute', this.startWorkflowNextHandler.bind(this));
const context = this.workflowSerializationContext();
(0, stack_helpers_1.untrackPromise)((0, logs_1.executeWithLifecycleLogging)(() => execute({
headers: activation.headers ?? {},
args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.arguments, context),
})).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this)));
}
initializeWorkflow(activation) {
const { continuedFailure, lastCompletionResult, memo, searchAttributes } = activation;
const context = this.workflowSerializationContext();
// Most things related to initialization have already been handled in the constructor
this.mutateWorkflowInfo((info) => ({
...info,
searchAttributes: (0, payload_search_attributes_1.decodeSearchAttributes)(searchAttributes?.indexedFields),
typedSearchAttributes: (0, payload_search_attributes_1.decodeTypedSearchAttributes)(searchAttributes?.indexedFields),
memo: (0, common_1.mapFromPayloads)(this.payloadConverter, memo?.fields, context),
lastResult: (0, common_1.fromPayloadsAtIndex)(this.payloadConverter, 0, lastCompletionResult?.payloads, context),
lastFailure: continuedFailure != null
? this.failureConverter.failureToError(continuedFailure, this.payloadConverter, context)
: undefined,
}));
const workflowDefinitionOpts = this.workflowDefinitionOptionsGetter?.();
if (workflowDefinitionOpts) {
this.versioningBehavior = workflowDefinitionOpts.versioningBehavior;
this.workflowDefinitionFailureExceptionTypes = workflowDefinitionOpts.failureExceptionTypes;
}
}
cancelWorkflow(_activation) {
this.cancelled = true;
this.rootScope.cancel();
}
fireTimer(activation) {
// Timers are a special case where their completion might not be in Workflow state,
// this is due to immediate timer cancellation that doesn't go wait for Core.
const completion = this.maybeConsumeCompletion('timer', getSeq(activation));
completion?.resolve(undefined);
}
resolveActivity(activation) {
if (!activation.result) {
throw new TypeError('Got ResolveActivity activation with no result');
}
const { resolve, reject, context } = this.consumeCompletion('activity', getSeq(activation));
if (activation.result.completed) {
const completed = activation.result.completed;
const result = completed.result ? this.payloadConverter.fromPayload(completed.result, context) : undefined;
resolve(result);
}
else if (activation.result.failed) {
const { failure } = activation.result.failed;
if (failure == null) {
throw new TypeError('Got failed result with no failure attribute');
}
reject(this.failureConverter.failureToError(failure, this.payloadConverter, context));
}
else if (activation.result.cancelled) {
const { failure } = activation.result.cancelled;
if (failure == null) {
throw new TypeError('Got cancelled result with no failure attribute');
}
reject(this.failureConverter.failureToError(failure, this.payloadConverter, context));
}
else if (activation.result.backoff) {
reject(new errors_1.LocalActivityDoBackoff(activation.result.backoff));
}
}
resolveChildWorkflowExecutionStart(activation) {
const { resolve, reject, context } = this.consumeCompletion('childWorkflowStart', getSeq(activation));
if (activation.succeeded) {
if (!activation.succeeded.runId) {
throw new TypeError('Got ResolveChildWorkflowExecutionStart with no runId');
}
resolve(activation.succeeded.runId);
}
else if (activation.failed) {
if (decodeStartChildWorkflowExecutionFailedCause(activation.failed.cause) !== 'WORKFLOW_ALREADY_EXISTS') {
throw new common_1.IllegalStateError('Got unknown StartChildWorkflowExecutionFailedCause');
}
if (!(activation.seq && activation.failed.workflowId && activation.failed.workflowType)) {
throw new TypeError('Missing attributes in activation job');
}
reject(new common_1.WorkflowExecutionAlreadyStartedError('Workflow execution already started', activation.failed.workflowId, activation.failed.workflowType));
}
else if (activation.cancelled) {
if (!activation.cancelled.failure) {
throw new TypeError('Got no failure in cancelled variant');
}
reject(this.failureConverter.failureToError(activation.cancelled.failure, this.payloadConverter, context));
}
else {
throw new TypeError('Got ResolveChildWorkflowExecutionStart with no status');
}
}
resolveChildWorkflowExecution(activation) {
if (!activation.result) {
throw new TypeError('Got ResolveChildWorkflowExecution activation with no result');
}
const { resolve, reject, context } = this.consumeCompletion('childWorkflowComplete', getSeq(activation));
if (activation.result.completed) {
const completed = activation.result.completed;
const result = completed.result ? this.payloadConverter.fromPayload(completed.result, context) : undefined;
resolve(result);
}
else if (activation.result.failed) {
const { failure } = activation.result.failed;
if (failure == null) {
throw new TypeError('Got failed result with no failure attribute');
}
reject(this.failureConverter.failureToError(failure, this.payloadConverter, context));
}
else if (activation.result.cancelled) {
const { failure } = activation.result.cancelled;
if (failure == null) {
throw new TypeError('Got cancelled result with no failure attribute');
}
reject(this.failureConverter.failureToError(failure, this.payloadConverter, context));
}
}
resolveNexusOperationStart(activation) {
const seq = getSeq(activation);
const { resolve, reject } = this.consumeCompletion('nexusOperationStart', seq);
if (!activation.failed) {
const completePromise = new Promise((resolve, reject) => {
this.completions.nexusOperationComplete.set(seq, {
resolve,
reject,
});
});
(0, stack_helpers_1.untrackPromise)(completePromise);
(0, stack_helpers_1.untrackPromise)(completePromise.catch(() => undefined));
resolve({ token: activation.operationToken, result: completePromise });
}
else {
reject(this.failureToError(activation.failed));
}
}
resolveNexusOperation(activation) {
const seq = getSeq(activation);
const context = this.workflowSerializationContext();
if (activation.result?.completed) {
const result = this.payloadConverter.fromPayload(activation.result.completed, context);
// It is possible for ResolveNexusOperation to be received without a prior ResolveNexusOperationStart,
// e.g. because the handler completed the Operation synchronously.
const startCompletion = this.maybeConsumeCompletion('nexusOperationStart', seq);
if (startCompletion) {
startCompletion.resolve({ result: Promise.resolve(result) });
}
else {
this.consumeCompletion('nexusOperationComplete', seq).resolve(result);
}
}
else {
let err;
if (activation.result?.failed) {
err = this.failureToError(activation.result.failed);
}
else if (activation.result?.cancelled) {
err = this.failureToError(activation.result.cancelled);
}
else if (activation.result?.timedOut) {
err = this.failureToError(activation.result.timedOut);
}
const completion = this.maybeConsumeCompletion('nexusOperationStart', seq) ??
this.consumeCompletion('nexusOperationComplete', seq);
completion.reject(err);
}
}
// Intentionally non-async function so this handler doesn't show up in the stack trace
queryWorkflowNextHandler({ queryName, args }) {
let fn = this.queryHandlers.get(queryName)?.handler;
if (fn === undefined && this.defaultQueryHandler !== undefined) {
fn = this.defaultQueryHandler.bind(undefined, queryName);
}
// No handler or default registered, fail.
if (fn === undefined) {
const knownQueryTypes = [...this.queryHandlers.keys()].join(' ');
// Fail the query
return Promise.reject(new ReferenceError(`Workflow did not register a handler for ${queryName}. Registered queries: [${knownQueryTypes}]`));
}
// Execute handler.
try {
const ret = fn(...args);
if (ret instanceof Promise) {
return Promise.reject(new errors_1.DeterminismViolationError('Query handlers should not return a Promise'));
}
return Promise.resolve(ret);
}
catch (err) {
return Promise.reject(err);
}
}
queryWorkflow(activation) {
const { queryType, queryId, headers } = activation;
if (!(queryType && queryId)) {
throw new TypeError('Missing query activation attributes');
}
// Reject __temporal_-prefixed queries that would otherwise be routed to the
// user's default handler. A specific registered handler (e.g. from a
// contrib package) is allowed through.
if (queryType.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) &&
!this.queryHandlers.has(queryType) &&
this.defaultQueryHandler !== undefined) {
throw new TypeError(`Cannot use query name: '${queryType}', with reserved prefix: '${reserved_1.TEMPORAL_RESERVED_PREFIX}'`);
}
// Skip interceptors if it's an internal query.
const isInternalQuery = queryType.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) ||
queryType === reserved_1.STACK_TRACE_QUERY_NAME ||
queryType === reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME;
const interceptors = isInternalQuery ? [] : this.interceptors.inbound;
const execute = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'handleQuery', this.queryWorkflowNextHandler.bind(this));
const context = this.workflowSerializationContext();
execute({
queryName: queryType,
args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.arguments, context),
queryId,
headers: headers ?? {},
}).then((result) => this.completeQuery(queryId, result), (reason) => this.failQuery(queryId, reason));
}
doUpdate(activation) {
const { id: updateId, protocolInstanceId, name, headers, runValidator } = activation;
if (!updateId) {
throw new TypeError('Missing activation update id');
}
if (!name) {
throw new TypeError('Missing activation update name');
}
if (!protocolInstanceId) {
throw new TypeError('Missing activation update protocolInstanceId');
}
// Reject __temporal_-prefixed updates that would otherwise be routed to the
// user's default handler. A specific registered handler (e.g. from a
// contrib package) is allowed through, and unregistered names without a
// default handler fall through to the buffer-then-reject path below.
if (name.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) &&
!this.updateHandlers.has(name) &&
this.defaultUpdateHandler !== undefined) {
throw new TypeError(`Cannot use update name: '${name}', with reserved prefix: '${reserved_1.TEMPORAL_RESERVED_PREFIX}'`);
}
// Skip interceptors if it's an internal update.
const isInternalUpdate = name.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) ||
name === reserved_1.STACK_TRACE_QUERY_NAME ||
name === reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME;
const interceptors = isInternalUpdate ? [] : this.interceptors.inbound;
const entry = this.updateHandlers.get(name) ??
(this.defaultUpdateHandler
? {
handler: this.defaultUpdateHandler.bind(undefined, name),
validator: undefined,
// Default to a warning policy.
unfinishedPolicy: common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON,
}
: null);
// If we don't have an entry from either source, buffer and return
if (entry == null) {
this.bufferedUpdates.push(activation);
return;
}
const makeInput = () => {
const context = this.workflowSerializationContext();
return {
updateId,
args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.input, context),
name,
headers: headers ?? {},
};
};
// The implementation below is responsible for upholding, and constrained
// by, the following contract:
//
// 1. If no validator is present then validation interceptors will not be run.
//
// 2. During validation, any error must fail the Update; during the Update
// itself, Temporal errors fail the Update whereas other errors fail the
// activation.
//
// 3. The handler must not see any mutations of the arguments made by the
// validator.
//
// 4. Any error when decoding/deserializing input must be caught and result
// in rejection of the Update before it is accepted, even if there is no
// validator.
//
// 5. The initial synchronous portion of the (async) Update handler should
// be executed after the (sync) validator completes such that there is
// minimal opportunity for a different concurrent task to be scheduled
// between them.
//
// 6. The stack trace view provided in the Temporal UI must not be polluted
// by promises that do not derive from user code. This implies that
// async/await syntax may not be used.
//
// Note that there is a deliberately unhandled promise rejection below.
// These are caught elsewhere and fail the corresponding activation.
const doUpdateImpl = async () => {
let input;
try {
if (runValidator && entry.validator) {
// Temporarily mark as not replaying history events during validator execution
// so that logging is permitted. Validators are live read-only operations.
const wasReplayingHistoryEvents = this.info.unsafe.isReplayingHistoryEvents;
this.mutateWorkflowInfo((info) => ({
...info,
unsafe: { ...info.unsafe, isReplayingHistoryEvents: false },
}));
try {
const validate = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'validateUpdate', this.validateUpdateNextHandler.bind(this, entry.validator));
validate(makeInput());
}
finally {
this.mutateWorkflowInfo((info) => ({
...info,
unsafe: { ...info.unsafe, isReplayingHistoryEvents: wasReplayingHistoryEvents },
}));
}
}
input = makeInput();
}
catch (error) {
this.rejectUpdate(protocolInstanceId, error);
return;
}
this.acceptUpdate(protocolInstanceId);
const execute = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'handleUpdate', this.updateNextHandler.bind(this, entry.handler));
const { unfinishedPolicy } = entry;
this.inProgressUpdates.set(updateId, { name, unfinishedPolicy, id: updateId });
const res = execute(input)
.then((result) => this.completeUpdate(protocolInstanceId, result))
.catch((error) => {
if (error instanceof common_1.TemporalFailure) {
this.rejectUpdate(protocolInstanceId, error);
}
else {
this.handleWorkflowFailure(error);
}
})
.finally(() => this.inProgressUpdates.delete(updateId));
(0, stack_helpers_1.untrackPromise)(res);
return res;
};
(0, stack_helpers_1.untrackPromise)(update_scope_1.UpdateScope.updateWithInfo(updateId, name, doUpdateImpl));
}
async updateNextHandler(handler, { args }) {
return await handler(...args);
}
validateUpdateNextHandler(validator, { args }) {
if (validator) {
validator(...args);
}
}
dispatchBufferedUpdates() {
const bufferedUpdates = this.bufferedUpdates;
while (bufferedUpdates.length) {
// We have a default update handler, so all updates are dispatchable.
if (this.defaultUpdateHandler) {
const update = bufferedUpdates.shift();
// Logically, this must be defined as we're in the loop.
// But Typescript doesn't know that so we use a non-null assertion (!).
this.doUpdate(update);
}
else {
const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name));
if (foundIndex === -1) {
// No buffered Updates have a handler yet.
break;
}
const [update] = bufferedUpdates.splice(foundIndex, 1);
this.doUpdate(update);
}
}
}
rejectBufferedUpdates() {
while (this.bufferedUpdates.length) {
const update = this.bufferedUpdates.shift();
if (update) {
this.rejectUpdate(update.protocolInstanceId, common_1.ApplicationFailure.nonRetryable(`No registered handler for update: ${update.name}`));
}
}
}
async signalWorkflowNextHandler({ signalName, args }) {
const fn = this.signalHandlers.get(signalName)?.handler;
if (fn) {
return await fn(...args);
}
else if (this.defaultSignalHandler) {
return await this.defaultSignalHandler(signalName, ...args);
}
else {
throw new common_1.IllegalStateError(`No registered signal handler for signal: ${signalName}`);
}
}
signalWorkflow(activation) {
const { signalName, headers } = activation;
if (!signalName) {
throw new TypeError('Missing activation signalName');
}
// Reject __temporal_-prefixed signals that would otherwise be routed to the
// user's default handler. A specific registered handler (e.g. from a
// contrib package) is allowed through, and unregistered names without a
// default handler fall through to the buffer-then-reject path below.
if (signalName.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) &&
!this.signalHandlers.has(signalName) &&
this.defaultSignalHandler !== undefined) {
throw new TypeError(`Cannot use signal name: '${signalName}', with reserved prefix: '${reserved_1.TEMPORAL_RESERVED_PREFIX}'`);
}
// Skip interceptors if it's an internal signal.
const isInternalSignal = signalName.startsWith(reserved_1.TEMPORAL_RESERVED_PREFIX) ||
signalName === reserved_1.STACK_TRACE_QUERY_NAME ||
signalName === reserved_1.ENHANCED_STACK_TRACE_QUERY_NAME;
const interceptors = isInternalSignal ? [] : this.interceptors.inbound;
if (!this.signalHandlers.has(signalName) && !this.defaultSignalHandler) {
this.bufferedSignals.push(activation);
return;
}
// If we fall through to the default signal handler then the unfinished
// policy is WARN_AND_ABANDON; users currently have no way to silence any
// ensuing warnings.
const unfinishedPolicy = this.signalHandlers.get(signalName)?.unfinishedPolicy ?? common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON;
const signalExecutionNum = this.signalHandlerExecutionSeq++;
this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy });
const execute = (0, interceptor_composition_1.composeInterceptors)(interceptors, 'handleSignal', this.signalWorkflowNextHandler.bind(this));
const context = this.workflowSerializationContext();
execute({
args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.input, context),
signalName,
headers: headers ?? {},
})
.catch(this.handleWorkflowFailure.bind(this))
.finally(() => this.inProgressSignals.delete(signalExecutionNum));
}
dispatchBufferedSignals() {
const bufferedSignals = this.bufferedSignals;
while (bufferedSignals.length) {
if (this.defaultSignalHandler) {
// We have a default signal handler, so all signals are dispatchable
this.signalWorkflow(bufferedSignals.shift());
}
else {
const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName));
if (foundIndex === -1)
break;
const [signal] = bufferedSignals.splice(foundIndex, 1);
this.signalWorkflow(signal);
}
}
}
resolveSignalExternalWorkflow(activation) {
const { resolve, reject, context } = this.consumeCompletion('signalWorkflow', getSeq(activation));
if (activation.failure) {
reject(this.failureConverter.failureToError(activation.failure, this.payloadConverter, context));
}
else {
resolve(undefined);
}
}
resolveRequestCancelExternalWorkflow(activation) {
const { resolve, reject, context } = this.consumeCompletion('cancelWorkflow', getSeq(activation));
if (activation.failure) {
reject(this.failureConverter.failureToError(activation.failure, this.payloadConverter, context));
}
else {
resolve(undefined);
}
}
warnIfUnfinishedHandlers() {
if (this.workflowTaskError)
return;
const getWarnable = (handlerExecutions) => {
return Array.from(handlerExecutions).filter((ex) => ex.unfinishedPolicy === common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON);
};
const warnableUpdates = getWarnable(this.inProgressUpdates.values());
if (warnableUpdates.length > 0) {
logs_1.log.warn(makeUnfinishedUpdateHandlerMessage(warnableUpdates));
}
const warnableSignals = getWarnable(this.inProgressSignals.values());
if (warnableSignals.length > 0) {
logs_1.log.warn(makeUnfinishedSignalHandlerMessage(warnableSignals));
}
}
updateRandomSeed(activation) {
if (!activation.randomnessSeed) {
throw new TypeError('Expected activation with randomnessSeed attribute');
}
this.setRandomnessSeed(activation.randomnessSeed.toBytes());
}
notifyHasPatch(activation) {
if (!this.info.unsafe.isReplaying)
throw new common_1.IllegalStateError('Unexpected notifyHasPatch job on non-replay activation');
if (!activation.patchId)
throw new TypeError('notifyHasPatch missing patch id');
this.knownPresentPatches.add(activation.patchId);
}
patchInternal(patchId, deprecated) {
if (this.workflow === undefined) {
throw new common_1.IllegalStateError('Patches cannot be used before Workflow starts');
}
const usePatch = !this.info.unsafe.isReplaying || this.knownPresentPatches.has(patchId);
// Avoid sending commands for patches core already knows about.
// This optimization enables development of automatic patching tools.
if (usePatch && !this.sentPatches.has(patchId)) {
this.pushCommand({
setPatchMarker: { patchId, deprecated },
});
this.sentPatches.add(patchId);
}
return usePatch;
}
/**
* Called early while handling an activation to register known flags.
* May be invoked from outside the VM.
*/
addKnownFlags(flags) {
for (const flag of flags) {
(0, flags_1.assertValidFlag)(flag);
this.knownFlags.add(flag);
}
}
/**
* Check if an SDK Flag may be considered as enabled for the current Workflow Task.
*
* SDK flags play a role similar to the `patched()` API, but are meant for internal usage by the
* SDK itself. They make it possible for the SDK to evolve its behaviors over time, while still
* maintaining compatibility with Workflow histories produced by older SDKs, without causing
* determinism violations.
*
* May be invoked from outside the VM.
*/
hasFlag(flag) {
if (this.knownFlags.has(flag.id))
return true;
// If not replaying, enable the flag if it is configured to be enabled by default. Setting a
// flag's default to false allows progressive rollout of new feature flags, with the possibility
// of reverting back to a version of the SDK where the flag is supported but disabled by default.
// It is also useful for testing purpose.
if (!this.info.unsafe.isReplaying && flag.default) {
this.knownFlags.add(flag.id);
return true;
}
// When replaying, a flag is considered enabled if it was enabled during the original execution of
// that Workflow Task; this is normally determined by the presence of the flag ID in the corresponding
// WFT Completed's `sdkMetadata.langUsedFlags`.
//
// SDK Flag Alternate Condition provides an alternative way of determining whether a flag should
// be considered as enabled for the current WFT; e.g. by looking at the version of the SDK that
// emitted a WFT. The main use case for this is to retroactively turn on some flags for WFT emitted
// by previous SDKs that contained a bug. Alt Conditions should only be used as a last resort.
//
// Note that conditions are only evaluated while replaying. Also, alternate conditions will not
// cause the flag to be persisted to the "used flags" set, which means that further Workflow Tasks
// may not reflect this flag if the condition no longer holds. This is so to avoid incorrect
// behaviors in case where a Workflow Execution has gone through a newer SDK version then again
// through an older one.
if (this.info.unsafe.isReplaying && flag.alternativeConditions) {
for (const cond of flag.alternativeConditions) {
if (cond({ info: this.info, sdkVersion: this.sdkVersion }))
return true;
}
}
return false;
}
removeFromCache() {
throw new common_1.IllegalStateError('removeFromCache activation job should not reach workflow');
}
/**
* Transforms failures into a command to be sent to the server.
* Used to handle any failure emitted by the Workflow.
*/
handleWorkflowFailure(error) {
if (this.cancelled && (0, errors_1.isCancellation)(error)) {
this.pushCommand({ cancelWorkflowExecution: {} }, true);
}
else if (error instanceof interfaces_1.ContinueAsNew) {
this.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
}
else if (error instanceof common_1.TemporalFailure || this.isConfiguredFailureException(error)) {
// Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we
// mark all handlers as completed now.
this.inProgressSignals.clear();
this.inProgressUpdates.clear();
this.pushCommand({
failWorkflowExecution: {
failure: this.errorToFailure((0, common_1.ensureTemporalFailure)(error)),
},
}, true);
}
else {
this.recordWorkflowTaskError(error);
}
}
/**
* Returns true if the given error matches any of the configured failure exception types
* (from {@link WorkerOptions.workflowFailureErrorTypes} or
* {@link WorkflowDefinitionOptions.failureExceptionTypes}).
*/
isConfiguredFailureException(error) {
// Check class references from WorkflowDefinitionOptions (instanceof-based, supports subclasses)
if (this.workflowDefinitionFailureExceptionTypes) {
// We guarantee that including Error in the list will catch _any_ error.
if (this.workflowDefinitionFailureExceptionTypes.includes(Error))
return true;
for (const errorType of this.workflowDefinitionFailureExceptionTypes) {
if (error instanceof errorType)
return true;
}
}
// Check class name strings from WorkerOptions (prototype-chain-based)
if (this.failureExceptionTypeNames.length > 0) {
// We guarantee that including 'Error' in the list will catch _any_ error.
if (this.failureExceptionTypeNames.includes('Error'))
return true;
if (typeof error === 'object' && error !== null) {
let ctor = error.constructor;
while (ctor != null && ctor !== Function.prototype) {
const name = ctor.name;
if (name) {
if (this.failureExceptionTypeNames.includes(name))
return true;
}
ctor = Object.getPrototypeOf(ctor);
}
}
}
return false;
}
recordWorkflowTaskError(error) {
// Only keep the first error that bubbles up; subsequent errors will be ignored.
if (this.workflowTaskError === undefined)
this.workflowTaskError = error;
// Immediately rethrow the error if we know it is safe to do so (i.e. we are not running async
// microtasks). Otherwise, the error will be rethrown whenever we get an opportunity to do so,
// e.g. the next time `tryUnblockConditions()` is called.
if (this.rethrowSynchronously)
this.maybeRethrowWorkflowTaskError();
}
/**
* If a Workflow Task error was captured, and we are running in synchronous mode,
* then bubble it up now. This is safe to call even if there is no error to rethrow.
*/
maybeRethrowWorkflowTaskError() {
if (this.workflowTaskError)
throw this.workflowTaskError;
}
completeQuery(queryId, result) {
const context = this.workflowSerializationContext();
this.pushCommand({
respondToQuery: { queryId, succeeded: { response: this.payloadConverter.toPayload(result, context) } },
});
}
failQuery(queryId, error) {
this.pushCommand({
respondToQuery: {
queryId,
failed: this.errorToFailure((0, common_1.ensureTemporalFailure)(error)),
},
});
}
acceptUpdate(pr