@temporalio/workflow
Version:
Temporal.io SDK Workflow sub-package
210 lines • 10.2 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _CancellationScope_cancelRequested;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RootCancellationScope = exports.CancellationScope = exports.AsyncLocalStorage = void 0;
exports.disableStorage = disableStorage;
exports.registerSleepImplementation = registerSleepImplementation;
const common_1 = require("@temporalio/common");
const time_1 = require("@temporalio/common/lib/time");
const stack_helpers_1 = require("./stack-helpers");
const global_attributes_1 = require("./global-attributes");
const flags_1 = require("./flags");
// AsyncLocalStorage is injected via vm module into global scope.
// In case Workflow code is imported in Node.js context, replace with an empty class.
exports.AsyncLocalStorage = globalThis.AsyncLocalStorage ?? class {
};
/** Magic symbol used to create the root scope - intentionally not exported */
const NO_PARENT = Symbol('NO_PARENT');
/**
* Cancellation Scopes provide the mechanic by which a Workflow may gracefully handle incoming requests for cancellation
* (e.g. in response to {@link WorkflowHandle.cancel} or through the UI or CLI), as well as request cancelation of
* cancellable operations it owns (e.g. Activities, Timers, Child Workflows, etc).
*
* Cancellation Scopes form a tree, with the Workflow's main function running in the root scope of that tree.
* By default, cancellation propagates down from a parent scope to its children and its cancellable operations.
* A non-cancellable scope can receive cancellation requests, but is never effectively considered as cancelled,
* thus shieldding its children and cancellable operations from propagation of cancellation requests it receives.
*
* Scopes are created using the `CancellationScope` constructor or the static helper methods {@link cancellable},
* {@link nonCancellable} and {@link withTimeout}. `withTimeout` creates a scope that automatically cancels itself after
* some duration.
*
* Cancellation of a cancellable scope results in all operations created directly in that scope to throw a
* {@link CancelledFailure} (either directly, or as the `cause` of an {@link ActivityFailure} or a
* {@link ChildWorkflowFailure}). Further attempt to create new cancellable scopes or cancellable operations within a
* scope that has already been cancelled will also immediately throw a {@link CancelledFailure} exception. It is however
* possible to create a non-cancellable scope at that point; this is often used to execute rollback or cleanup
* operations. For example:
*
* ```ts
* async function myWorkflow(...): Promise<void> {
* try {
* // This activity runs in the root cancellation scope. Therefore, a cancelation request on
* // the Workflow execution (e.g. through the UI or CLI) automatically propagates to this
* // activity. Assuming that the activity properly handle the cancellation request, then the
* // call below will throw an `ActivityFailure` exception, with `cause` sets to an
* // instance of `CancelledFailure`.
* await someActivity();
* } catch (e) {
* if (isCancellation(e)) {
* // Run cleanup activity in a non-cancellable scope
* await CancellationScope.nonCancellable(async () => {
* await cleanupActivity();
* }
* } else {
* throw e;
* }
* }
* }
* ```
*
* A cancellable scope may be programatically cancelled by calling {@link cancel|`scope.cancel()`}`. This may be used,
* for example, to explicitly request cancellation of an Activity or Child Workflow:
*
* ```ts
* const cancellableActivityScope = new CancellationScope();
* const activityPromise = cancellableActivityScope.run(() => someActivity());
* cancellableActivityScope.cancel(); // Cancels the activity
* await activityPromise; // Throws `ActivityFailure` with `cause` set to `CancelledFailure`
* ```
*/
class CancellationScope {
constructor(options) {
_CancellationScope_cancelRequested.set(this, false);
this.timeout = (0, time_1.msOptionalToNumber)(options?.timeout);
this.cancellable = options?.cancellable ?? true;
this.cancelRequested = new Promise((_, reject) => {
// @ts-expect-error TSC doesn't understand that the Promise executor runs synchronously
this.reject = (err) => {
__classPrivateFieldSet(this, _CancellationScope_cancelRequested, true, "f");
reject(err);
};
});
(0, stack_helpers_1.untrackPromise)(this.cancelRequested);
// Avoid unhandled rejections
(0, stack_helpers_1.untrackPromise)(this.cancelRequested.catch(() => undefined));
if (options?.parent !== NO_PARENT) {
this.parent = options?.parent || CancellationScope.current();
if (this.parent.cancellable ||
(__classPrivateFieldGet(this.parent, _CancellationScope_cancelRequested, "f") &&
!(0, global_attributes_1.getActivator)().hasFlag(flags_1.SdkFlags.NonCancellableScopesAreShieldedFromPropagation))) {
__classPrivateFieldSet(this, _CancellationScope_cancelRequested, __classPrivateFieldGet(this.parent, _CancellationScope_cancelRequested, "f"), "f");
(0, stack_helpers_1.untrackPromise)(this.parent.cancelRequested.catch((err) => {
this.reject(err);
}));
}
else {
(0, stack_helpers_1.untrackPromise)(this.parent.cancelRequested.catch((err) => {
if (!(0, global_attributes_1.getActivator)().hasFlag(flags_1.SdkFlags.NonCancellableScopesAreShieldedFromPropagation)) {
this.reject(err);
}
}));
}
}
}
/**
* Whether the scope was effectively cancelled. A non-cancellable scope can never be considered cancelled.
*/
get consideredCancelled() {
return __classPrivateFieldGet(this, _CancellationScope_cancelRequested, "f") && this.cancellable;
}
/**
* Activate the scope as current and run `fn`
*
* Any timers, Activities, Triggers and CancellationScopes created in the body of `fn`
* automatically link their cancellation to this scope.
*
* @return the result of `fn`
*/
run(fn) {
return storage.run(this, this.runInContext.bind(this, fn));
}
/**
* Method that runs a function in AsyncLocalStorage context.
*
* Could have been written as anonymous function, made into a method for improved stack traces.
*/
async runInContext(fn) {
let timerScope;
if (this.timeout) {
timerScope = new CancellationScope();
(0, stack_helpers_1.untrackPromise)(timerScope
.run(() => sleep(this.timeout))
.then(() => this.cancel(), () => {
// scope was already cancelled, ignore
}));
}
try {
return await fn();
}
finally {
if (timerScope &&
!timerScope.consideredCancelled &&
(0, global_attributes_1.getActivator)().hasFlag(flags_1.SdkFlags.NonCancellableScopesAreShieldedFromPropagation)) {
timerScope.cancel();
}
}
}
/**
* Request to cancel the scope and linked children
*/
cancel() {
this.reject(new common_1.CancelledFailure('Cancellation scope cancelled'));
}
/**
* Get the current "active" scope
*/
static current() {
// Using globals directly instead of a helper function to avoid circular import
return storage.getStore() ?? globalThis.__TEMPORAL_ACTIVATOR__.rootScope;
}
/** Alias to `new CancellationScope({ cancellable: true }).run(fn)` */
static cancellable(fn) {
return new this({ cancellable: true }).run(fn);
}
/** Alias to `new CancellationScope({ cancellable: false }).run(fn)` */
static nonCancellable(fn) {
return new this({ cancellable: false }).run(fn);
}
/** Alias to `new CancellationScope({ cancellable: true, timeout }).run(fn)` */
static withTimeout(timeout, fn) {
return new this({ cancellable: true, timeout }).run(fn);
}
}
exports.CancellationScope = CancellationScope;
_CancellationScope_cancelRequested = new WeakMap();
const storage = new exports.AsyncLocalStorage();
/**
* Avoid exposing the storage directly so it doesn't get frozen
*/
function disableStorage() {
storage.disable();
}
class RootCancellationScope extends CancellationScope {
constructor() {
super({ cancellable: true, parent: NO_PARENT });
}
cancel() {
this.reject(new common_1.CancelledFailure('Workflow cancelled'));
}
}
exports.RootCancellationScope = RootCancellationScope;
/** This function is here to avoid a circular dependency between this module and workflow.ts */
let sleep = (_) => {
throw new common_1.IllegalStateError('Workflow has not been properly initialized');
};
function registerSleepImplementation(fn) {
sleep = fn;
}
//# sourceMappingURL=cancellation-scope.js.map
;