UNPKG

@temporalio/workflow

Version:
964 lines 42.4 kB
"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 interceptors_1 = require("@temporalio/common/lib/interceptors"); const internal_workflow_1 = require("@temporalio/common/lib/internal-workflow"); const alea_1 = require("./alea"); const cancellation_scope_1 = require("./cancellation-scope"); const update_scope_1 = require("./update-scope"); 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(), 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; /** * 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([ [ '__stack_trace', { handler: () => { return this.getStackTraces() .map((s) => s.formatted) .join('\n\n'); }, description: 'Returns a sensible stack trace.', }, ], [ '__enhanced_stack_trace', { 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 { 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 { definition: { type: workflowType, queryDefinitions, signalDefinitions, updateDefinitions, }, }; }, 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, // 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; /** * A deterministic RNG, used by the isolate's overridden Math.random */ random; 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(); /** * 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; versioningBehavior; workflowDefinitionOptionsGetter; constructor({ info, now, showStackTraceSources, sourceMap, getTimeOfDay, randomnessSeed, registeredActivityNames, }) { this.getTimeOfDay = getTimeOfDay; this.info = info; this.now = now; this.showStackTraceSources = showStackTraceSources; this.sourceMap = sourceMap; this.random = (0, alea_1.alea)(randomnessSeed); this.registeredActivityNames = registeredActivityNames; } /** * May be invoked from outside the VM. */ mutateWorkflowInfo(fn) { this.info = fn(this.info); } getStackTraces() { 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 === undefined) { throw new common_1.IllegalStateError('Workflow uninitialized'); } return await workflow(...args); } startWorkflow(activation) { const execute = (0, interceptors_1.composeInterceptors)(this.interceptors.inbound, 'execute', this.startWorkflowNextHandler.bind(this)); (0, stack_helpers_1.untrackPromise)((0, logs_1.executeWithLifecycleLogging)(() => execute({ headers: activation.headers ?? {}, args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.arguments), })).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this))); } initializeWorkflow(activation) { const { continuedFailure, lastCompletionResult, memo, searchAttributes } = activation; // 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), lastResult: (0, common_1.fromPayloadsAtIndex)(this.payloadConverter, 0, lastCompletionResult?.payloads), lastFailure: continuedFailure != null ? this.failureConverter.failureToError(continuedFailure, this.payloadConverter) : undefined, })); if (this.workflowDefinitionOptionsGetter) { this.versioningBehavior = this.workflowDefinitionOptionsGetter().versioningBehavior; } } 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 } = this.consumeCompletion('activity', getSeq(activation)); if (activation.result.completed) { const completed = activation.result.completed; const result = completed.result ? this.payloadConverter.fromPayload(completed.result) : undefined; resolve(result); } else if (activation.result.failed) { const { failure } = activation.result.failed; const err = failure ? this.failureToError(failure) : undefined; reject(err); } else if (activation.result.cancelled) { const { failure } = activation.result.cancelled; const err = failure ? this.failureToError(failure) : undefined; reject(err); } else if (activation.result.backoff) { reject(new errors_1.LocalActivityDoBackoff(activation.result.backoff)); } } resolveChildWorkflowExecutionStart(activation) { const { resolve, reject } = this.consumeCompletion('childWorkflowStart', getSeq(activation)); if (activation.succeeded) { 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.failureToError(activation.cancelled.failure)); } 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 } = this.consumeCompletion('childWorkflowComplete', getSeq(activation)); if (activation.result.completed) { const completed = activation.result.completed; const result = completed.result ? this.payloadConverter.fromPayload(completed.result) : undefined; resolve(result); } else if (activation.result.failed) { const { failure } = activation.result.failed; if (failure === undefined || failure === null) { throw new TypeError('Got failed result with no failure attribute'); } reject(this.failureToError(failure)); } else if (activation.result.cancelled) { const { failure } = activation.result.cancelled; if (failure === undefined || failure === null) { throw new TypeError('Got cancelled result with no failure attribute'); } reject(this.failureToError(failure)); } } resolveNexusOperationStart(_) { throw new Error('TODO'); } resolveNexusOperation(_) { throw new Error('TODO'); } // 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'); } const execute = (0, interceptors_1.composeInterceptors)(this.interceptors.inbound, 'handleQuery', this.queryWorkflowNextHandler.bind(this)); execute({ queryName: queryType, args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.arguments), 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'); } 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 = () => ({ updateId, args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.input), 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) { const validate = (0, interceptors_1.composeInterceptors)(this.interceptors.inbound, 'validateUpdate', this.validateUpdateNextHandler.bind(this, entry.validator)); validate(makeInput()); } input = makeInput(); } catch (error) { this.rejectUpdate(protocolInstanceId, error); return; } this.acceptUpdate(protocolInstanceId); const execute = (0, interceptors_1.composeInterceptors)(this.interceptors.inbound, '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( /* eslint-disable @typescript-eslint/no-non-null-assertion */ 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'); } 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, interceptors_1.composeInterceptors)(this.interceptors.inbound, 'handleSignal', this.signalWorkflowNextHandler.bind(this)); execute({ args: (0, common_1.arrayFromPayloads)(this.payloadConverter, activation.input), 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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 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 } = this.consumeCompletion('signalWorkflow', getSeq(activation)); if (activation.failure) { reject(this.failureToError(activation.failure)); } else { resolve(undefined); } } resolveRequestCancelExternalWorkflow(activation) { const { resolve, reject } = this.consumeCompletion('cancelWorkflow', getSeq(activation)); if (activation.failure) { reject(this.failureToError(activation.failure)); } 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.random = (0, alea_1.alea)(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 })) 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) { // 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(error), }, }, true); } else { this.recordWorkflowTaskError(error); } } 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) { this.pushCommand({ respondToQuery: { queryId, succeeded: { response: this.payloadConverter.toPayload(result) } }, }); } failQuery(queryId, error) { this.pushCommand({ respondToQuery: { queryId, failed: this.errorToFailure((0, common_1.ensureTemporalFailure)(error)), }, }); } acceptUpdate(protocolInstanceId) { this.pushCommand({ updateResponse: { protocolInstanceId, accepted: {} } }); } completeUpdate(protocolInstanceId, result) { this.pushCommand({ updateResponse: { protocolInstanceId, completed: this.payloadConverter.toPayload(result) }, }); } rejectUpdate(protocolInstanceId, error) { this.pushCommand({ updateResponse: { protocolInstanceId, rejected: this.errorToFailure((0, common_1.ensureTemporalFailure)(error)), }, }); } /** Consume a completion if it exists in Workflow state */ maybeConsumeCompletion(type, taskSeq) { const completion = this.completions[type].get(taskSeq); if (completion !== undefined) { this.completions[type].delete(taskSeq); } return completion; } /** Consume a completion if it exists in Workflow state, throws if it doesn't */ consumeCompletion(type, taskSeq) { const completion = this.maybeConsumeCompletion(type, taskSeq); if (completion === undefined) { throw new common_1.IllegalStateError(`No completion for taskSeq ${taskSeq}`); } return completion; } completeWorkflow(result) { this.pushCommand({ completeWorkflowExecution: { result: this.payloadConverter.toPayload(result), }, }, true); } errorToFailure(err) { return this.failureConverter.errorToFailure(err, this.payloadConverter); } failureToError(failure) { return this.failureConverter.failureToError(failure, this.payloadConverter); } } exports.Activator = Activator; function getSeq(activation) { const seq = activation.seq; if (seq === undefined || seq === null) { throw new TypeError(`Got activation with no seq attribute`); } return seq; } function makeUnfinishedUpdateHandlerMessage(handlerExecutions) { const message = ` [TMPRL1102] Workflow finished while an update handler was still running. This may have interrupted work that the update handler was doing, and the client that sent the update will receive a 'workflow execution already completed' RPCError instead of the update result. You can wait for all update and signal handlers to complete by using \`await workflow.condition(workflow.allHandlersFinished)\`. Alternatively, if both you and the clients sending the update are okay with interrupting running handlers when the workflow finishes, and causing clients to receive errors, then you can disable this warning by passing an option when setting the handler: \`workflow.setHandler(myUpdate, myUpdateHandler, {unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON});\`.` .replace(/\n/g, ' ') .trim(); return `${message} The following updates were unfinished (and warnings were not disabled for their handler): ${JSON.stringify(handlerExecutions.map((ex) => ({ name: ex.name, id: ex.id })))}`; } function makeUnfinishedSignalHandlerMessage(handlerExecutions) { const message = ` [TMPRL1102] Workflow finished while a signal handler was still running. This may have interrupted work that the signal handler was doing. You can wait for all update and signal handlers to complete by using \`await workflow.condition(workflow.allHandlersFinished)\`. Alternatively, if both you and the clients sending the update are okay with interrupting running handlers when the workflow finishes, then you can disable this warning by passing an option when setting the handler: \`workflow.setHandler(mySignal, mySignalHandler, {unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON});\`.` .replace(/\n/g, ' ') .trim(); const names = new Map(); for (const ex of handlerExecutions) { const count = names.get(ex.name) || 0; names.set(ex.name, count + 1); } return `${message} The following signals were unfinished (and warnings were not disabled for their handler): ${JSON.stringify(Array.from(names.entries()).map(([name, count]) => ({ name, count })))}`; } //# sourceMappingURL=internals.js.map