@temporalio/workflow 1.10.1 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cancellation-scope.d.ts +59 -28
- package/lib/cancellation-scope.js +79 -33
- package/lib/cancellation-scope.js.map +1 -1
- package/lib/flags.d.ts +18 -0
- package/lib/flags.js +28 -0
- package/lib/flags.js.map +1 -0
- package/lib/global-overrides.d.ts +1 -0
- package/lib/global-overrides.js +97 -0
- package/lib/global-overrides.js.map +1 -0
- package/lib/interfaces.d.ts +4 -0
- package/lib/internals.d.ts +9 -4
- package/lib/internals.js +39 -4
- package/lib/internals.js.map +1 -1
- package/lib/trigger.js +1 -1
- package/lib/trigger.js.map +1 -1
- package/lib/worker-interface.d.ts +0 -1
- package/lib/worker-interface.js +5 -58
- package/lib/worker-interface.js.map +1 -1
- package/lib/workflow.js +5 -21
- package/lib/workflow.js.map +1 -1
- package/package.json +4 -4
- package/src/cancellation-scope.ts +107 -43
- package/src/flags.ts +30 -0
- package/src/global-overrides.ts +108 -0
- package/src/interfaces.ts +5 -0
- package/src/internals.ts +46 -6
- package/src/trigger.ts +1 -1
- package/src/worker-interface.ts +5 -70
- package/src/workflow.ts +6 -23
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { AsyncLocalStorage as ALS } from 'node:async_hooks';
|
|
2
2
|
import { CancelledFailure, Duration, IllegalStateError } from '@temporalio/common';
|
|
3
|
+
import { msOptionalToNumber } from '@temporalio/common/lib/time';
|
|
3
4
|
import { untrackPromise } from './stack-helpers';
|
|
5
|
+
import { getActivator } from './global-attributes';
|
|
6
|
+
import { SdkFlags } from './flags';
|
|
4
7
|
|
|
5
8
|
// AsyncLocalStorage is injected via vm module into global scope.
|
|
6
9
|
// In case Workflow code is imported in Node.js context, replace with an empty class.
|
|
@@ -16,7 +19,7 @@ export interface CancellationScopeOptions {
|
|
|
16
19
|
/**
|
|
17
20
|
* Time in milliseconds before the scope cancellation is automatically requested
|
|
18
21
|
*/
|
|
19
|
-
timeout?:
|
|
22
|
+
timeout?: Duration;
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* If false, prevent outer cancellation from propagating to inner scopes, Activities, timers, and Triggers, defaults to true.
|
|
@@ -31,38 +34,56 @@ export interface CancellationScopeOptions {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
+
* Cancellation Scopes provide the mechanic by which a Workflow may gracefully handle incoming requests for cancellation
|
|
38
|
+
* (e.g. in response to {@link WorkflowHandle.cancel} or through the UI or CLI), as well as request cancelation of
|
|
39
|
+
* cancellable operations it owns (e.g. Activities, Timers, Child Workflows, etc).
|
|
37
40
|
*
|
|
38
|
-
* Scopes
|
|
39
|
-
*
|
|
41
|
+
* Cancellation Scopes form a tree, with the Workflow's main function running in the root scope of that tree.
|
|
42
|
+
* By default, cancellation propagates down from a parent scope to its children and its cancellable operations.
|
|
43
|
+
* A non-cancellable scope can receive cancellation requests, but is never effectively considered as cancelled,
|
|
44
|
+
* thus shieldding its children and cancellable operations from propagation of cancellation requests it receives.
|
|
40
45
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Scopes are created using the `CancellationScope` constructor or the static helper methods {@link cancellable},
|
|
47
|
+
* {@link nonCancellable} and {@link withTimeout}. `withTimeout` creates a scope that automatically cancels itself after
|
|
48
|
+
* some duration.
|
|
43
49
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
+
* Cancellation of a cancellable scope results in all operations created directly in that scope to throw a
|
|
51
|
+
* {@link CancelledFailure} (either directly, or as the `cause` of an {@link ActivityFailure} or a
|
|
52
|
+
* {@link ChildWorkflowFailure}). Further attempt to create new cancellable scopes or cancellable operations within a
|
|
53
|
+
* scope that has already been cancelled will also immediately throw a {@link CancelledFailure} exception. It is however
|
|
54
|
+
* possible to create a non-cancellable scope at that point; this is often used to execute rollback or cleanup
|
|
55
|
+
* operations. For example:
|
|
50
56
|
*
|
|
51
57
|
* ```ts
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
58
|
+
* async function myWorkflow(...): Promise<void> {
|
|
59
|
+
* try {
|
|
60
|
+
* // This activity runs in the root cancellation scope. Therefore, a cancelation request on
|
|
61
|
+
* // the Workflow execution (e.g. through the UI or CLI) automatically propagates to this
|
|
62
|
+
* // activity. Assuming that the activity properly handle the cancellation request, then the
|
|
63
|
+
* // call below will throw an `ActivityFailure` exception, with `cause` sets to an
|
|
64
|
+
* // instance of `CancelledFailure`.
|
|
65
|
+
* await someActivity();
|
|
66
|
+
* } catch (e) {
|
|
67
|
+
* if (isCancellation(e)) {
|
|
68
|
+
* // Run cleanup activity in a non-cancellable scope
|
|
69
|
+
* await CancellationScope.nonCancellable(async () => {
|
|
70
|
+
* await cleanupActivity();
|
|
71
|
+
* }
|
|
72
|
+
* } else {
|
|
73
|
+
* throw e;
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
57
77
|
* ```
|
|
58
78
|
*
|
|
59
|
-
* @
|
|
79
|
+
* A cancellable scope may be programatically cancelled by calling {@link cancel|`scope.cancel()`}`. This may be used,
|
|
80
|
+
* for example, to explicitly request cancellation of an Activity or Child Workflow:
|
|
60
81
|
*
|
|
61
82
|
* ```ts
|
|
62
|
-
* const
|
|
63
|
-
* const
|
|
64
|
-
*
|
|
65
|
-
* await
|
|
83
|
+
* const cancellableActivityScope = new CancellationScope();
|
|
84
|
+
* const activityPromise = cancellableActivityScope.run(() => someActivity());
|
|
85
|
+
* cancellableActivityScope.cancel(); // Cancels the activity
|
|
86
|
+
* await activityPromise; // Throws `ActivityFailure` with `cause` set to `CancelledFailure`
|
|
66
87
|
* ```
|
|
67
88
|
*/
|
|
68
89
|
export class CancellationScope {
|
|
@@ -72,17 +93,28 @@ export class CancellationScope {
|
|
|
72
93
|
protected readonly timeout?: number;
|
|
73
94
|
|
|
74
95
|
/**
|
|
75
|
-
* If false,
|
|
76
|
-
* (
|
|
96
|
+
* If false, then this scope will never be considered cancelled, even if a cancellation request is received (either
|
|
97
|
+
* directly by calling `scope.cancel()` or indirectly by cancelling a cancellable parent scope). This effectively
|
|
98
|
+
* shields the scope's children and cancellable operations from propagation of cancellation requests made on the
|
|
99
|
+
* non-cancellable scope.
|
|
100
|
+
*
|
|
101
|
+
* Note that the Promise returned by the `run` function of non-cancellable scope may still throw a `CancelledFailure`
|
|
102
|
+
* if such an exception is thrown from within that scope (e.g. by directly cancelling a cancellable child scope).
|
|
77
103
|
*/
|
|
78
104
|
public readonly cancellable: boolean;
|
|
105
|
+
|
|
79
106
|
/**
|
|
80
107
|
* An optional CancellationScope (useful for running background tasks), defaults to {@link CancellationScope.current}()
|
|
81
108
|
*/
|
|
82
109
|
public readonly parent?: CancellationScope;
|
|
83
110
|
|
|
84
111
|
/**
|
|
85
|
-
*
|
|
112
|
+
* A Promise that throws when a cancellable scope receives a cancellation request, either directly
|
|
113
|
+
* (i.e. `scope.cancel()`), or indirectly (by cancelling a cancellable parent scope).
|
|
114
|
+
*
|
|
115
|
+
* Note that a non-cancellable scope may receive cancellation requests, resulting in the `cancelRequested` promise for
|
|
116
|
+
* that scope to throw, though the scope will not effectively get cancelled (i.e. `consideredCancelled` will still
|
|
117
|
+
* return `false`, and cancellation will not be propagated to child scopes and contained operations).
|
|
86
118
|
*/
|
|
87
119
|
public readonly cancelRequested: Promise<never>;
|
|
88
120
|
|
|
@@ -94,12 +126,10 @@ export class CancellationScope {
|
|
|
94
126
|
protected readonly reject: (reason?: any) => void;
|
|
95
127
|
|
|
96
128
|
constructor(options?: CancellationScopeOptions) {
|
|
97
|
-
this.timeout = options?.timeout;
|
|
129
|
+
this.timeout = msOptionalToNumber(options?.timeout);
|
|
98
130
|
this.cancellable = options?.cancellable ?? true;
|
|
99
131
|
this.cancelRequested = new Promise((_, reject) => {
|
|
100
|
-
//
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
102
|
-
// @ts-ignore
|
|
132
|
+
// @ts-expect-error TSC doesn't understand that the Promise executor runs synchronously
|
|
103
133
|
this.reject = (err) => {
|
|
104
134
|
this.#cancelRequested = true;
|
|
105
135
|
reject(err);
|
|
@@ -110,16 +140,36 @@ export class CancellationScope {
|
|
|
110
140
|
untrackPromise(this.cancelRequested.catch(() => undefined));
|
|
111
141
|
if (options?.parent !== NO_PARENT) {
|
|
112
142
|
this.parent = options?.parent || CancellationScope.current();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
143
|
+
if (
|
|
144
|
+
this.parent.cancellable ||
|
|
145
|
+
(this.parent.#cancelRequested &&
|
|
146
|
+
!getActivator().hasFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation))
|
|
147
|
+
) {
|
|
148
|
+
this.#cancelRequested = this.parent.#cancelRequested;
|
|
149
|
+
untrackPromise(
|
|
150
|
+
this.parent.cancelRequested.catch((err) => {
|
|
151
|
+
this.reject(err);
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
untrackPromise(
|
|
156
|
+
this.parent.cancelRequested.catch((err) => {
|
|
157
|
+
if (!getActivator().hasFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation)) {
|
|
158
|
+
this.reject(err);
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
}
|
|
117
163
|
}
|
|
118
164
|
}
|
|
119
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Whether the scope was effectively cancelled. A non-cancellable scope can never be considered cancelled.
|
|
168
|
+
*/
|
|
120
169
|
public get consideredCancelled(): boolean {
|
|
121
170
|
return this.#cancelRequested && this.cancellable;
|
|
122
171
|
}
|
|
172
|
+
|
|
123
173
|
/**
|
|
124
174
|
* Activate the scope as current and run `fn`
|
|
125
175
|
*
|
|
@@ -138,17 +188,31 @@ export class CancellationScope {
|
|
|
138
188
|
* Could have been written as anonymous function, made into a method for improved stack traces.
|
|
139
189
|
*/
|
|
140
190
|
protected async runInContext<T>(fn: () => Promise<T>): Promise<T> {
|
|
191
|
+
let timerScope: CancellationScope | undefined;
|
|
141
192
|
if (this.timeout) {
|
|
193
|
+
timerScope = new CancellationScope();
|
|
142
194
|
untrackPromise(
|
|
143
|
-
|
|
144
|
-
() => this.
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
195
|
+
timerScope
|
|
196
|
+
.run(() => sleep(this.timeout as number))
|
|
197
|
+
.then(
|
|
198
|
+
() => this.cancel(),
|
|
199
|
+
() => {
|
|
200
|
+
// scope was already cancelled, ignore
|
|
201
|
+
}
|
|
202
|
+
)
|
|
149
203
|
);
|
|
150
204
|
}
|
|
151
|
-
|
|
205
|
+
try {
|
|
206
|
+
return await fn();
|
|
207
|
+
} finally {
|
|
208
|
+
if (
|
|
209
|
+
timerScope &&
|
|
210
|
+
!timerScope.consideredCancelled &&
|
|
211
|
+
getActivator().hasFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation)
|
|
212
|
+
) {
|
|
213
|
+
timerScope.cancel();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
152
216
|
}
|
|
153
217
|
|
|
154
218
|
/**
|
|
@@ -177,7 +241,7 @@ export class CancellationScope {
|
|
|
177
241
|
}
|
|
178
242
|
|
|
179
243
|
/** Alias to `new CancellationScope({ cancellable: true, timeout }).run(fn)` */
|
|
180
|
-
static withTimeout<T>(timeout:
|
|
244
|
+
static withTimeout<T>(timeout: Duration, fn: () => Promise<T>): Promise<T> {
|
|
181
245
|
return new this({ cancellable: true, timeout }).run(fn);
|
|
182
246
|
}
|
|
183
247
|
}
|
package/src/flags.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type SdkFlag = {
|
|
2
|
+
get id(): number;
|
|
3
|
+
get default(): boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const flagsRegistry: Map<number, SdkFlag> = new Map();
|
|
7
|
+
|
|
8
|
+
export const SdkFlags = {
|
|
9
|
+
/**
|
|
10
|
+
* This flag gates multiple fixes related to cancellation scopes and timers introduced in 1.10.2/1.11.0:
|
|
11
|
+
* - Cancellation of a non-cancellable scope no longer propagates to children scopes
|
|
12
|
+
* (see https://github.com/temporalio/sdk-typescript/issues/1423).
|
|
13
|
+
* - CancellationScope.withTimeout(fn) now cancel the timer if `fn` completes before expiration
|
|
14
|
+
* of the timeout, similar to how `condition(fn, timeout)` works.
|
|
15
|
+
* - Timers created using setTimeout can now be intercepted.
|
|
16
|
+
*
|
|
17
|
+
* @since Introduced in 1.10.2/1.11.0.
|
|
18
|
+
*/
|
|
19
|
+
NonCancellableScopesAreShieldedFromPropagation: defineFlag(1, false),
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
function defineFlag(id: number, def: boolean): SdkFlag {
|
|
23
|
+
const flag = { id, default: def };
|
|
24
|
+
flagsRegistry.set(id, flag);
|
|
25
|
+
return flag;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function assertValidFlag(id: number): void {
|
|
29
|
+
if (!flagsRegistry.has(id)) throw new TypeError(`Unknown SDK flag: ${id}`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overrides some global objects to make them deterministic.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { msToTs } from '@temporalio/common/lib/time';
|
|
7
|
+
import { CancellationScope } from './cancellation-scope';
|
|
8
|
+
import { DeterminismViolationError } from './errors';
|
|
9
|
+
import { getActivator } from './global-attributes';
|
|
10
|
+
import { SdkFlags } from './flags';
|
|
11
|
+
import { sleep } from './workflow';
|
|
12
|
+
import { untrackPromise } from './stack-helpers';
|
|
13
|
+
|
|
14
|
+
const global = globalThis as any;
|
|
15
|
+
const OriginalDate = globalThis.Date;
|
|
16
|
+
|
|
17
|
+
export function overrideGlobals(): void {
|
|
18
|
+
// Mock any weak reference because GC is non-deterministic and the effect is observable from the Workflow.
|
|
19
|
+
// Workflow developer will get a meaningful exception if they try to use these.
|
|
20
|
+
global.WeakRef = function () {
|
|
21
|
+
throw new DeterminismViolationError('WeakRef cannot be used in Workflows because v8 GC is non-deterministic');
|
|
22
|
+
};
|
|
23
|
+
global.FinalizationRegistry = function () {
|
|
24
|
+
throw new DeterminismViolationError(
|
|
25
|
+
'FinalizationRegistry cannot be used in Workflows because v8 GC is non-deterministic'
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
global.Date = function (...args: unknown[]) {
|
|
30
|
+
if (args.length > 0) {
|
|
31
|
+
return new (OriginalDate as any)(...args);
|
|
32
|
+
}
|
|
33
|
+
return new OriginalDate(getActivator().now);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
global.Date.now = function () {
|
|
37
|
+
return getActivator().now;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
global.Date.parse = OriginalDate.parse.bind(OriginalDate);
|
|
41
|
+
global.Date.UTC = OriginalDate.UTC.bind(OriginalDate);
|
|
42
|
+
|
|
43
|
+
global.Date.prototype = OriginalDate.prototype;
|
|
44
|
+
|
|
45
|
+
const timeoutCancelationScopes = new Map<number, CancellationScope>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param ms sleep duration - number of milliseconds. If given a negative number, value will be set to 1.
|
|
49
|
+
*/
|
|
50
|
+
global.setTimeout = function (cb: (...args: any[]) => any, ms: number, ...args: any[]): number {
|
|
51
|
+
ms = Math.max(1, ms);
|
|
52
|
+
const activator = getActivator();
|
|
53
|
+
if (activator.hasFlag(SdkFlags.NonCancellableScopesAreShieldedFromPropagation)) {
|
|
54
|
+
// Capture the sequence number that sleep will allocate
|
|
55
|
+
const seq = activator.nextSeqs.timer;
|
|
56
|
+
const timerScope = new CancellationScope({ cancellable: true });
|
|
57
|
+
const sleepPromise = timerScope.run(() => sleep(ms));
|
|
58
|
+
sleepPromise.then(
|
|
59
|
+
() => {
|
|
60
|
+
timeoutCancelationScopes.delete(seq);
|
|
61
|
+
cb(...args);
|
|
62
|
+
},
|
|
63
|
+
() => {
|
|
64
|
+
timeoutCancelationScopes.delete(seq);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
untrackPromise(sleepPromise);
|
|
68
|
+
timeoutCancelationScopes.set(seq, timerScope);
|
|
69
|
+
return seq;
|
|
70
|
+
} else {
|
|
71
|
+
const seq = activator.nextSeqs.timer++;
|
|
72
|
+
// Create a Promise for AsyncLocalStorage to be able to track this completion using promise hooks.
|
|
73
|
+
new Promise((resolve, reject) => {
|
|
74
|
+
activator.completions.timer.set(seq, { resolve, reject });
|
|
75
|
+
activator.pushCommand({
|
|
76
|
+
startTimer: {
|
|
77
|
+
seq,
|
|
78
|
+
startToFireTimeout: msToTs(ms),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}).then(
|
|
82
|
+
() => cb(...args),
|
|
83
|
+
() => undefined /* ignore cancellation */
|
|
84
|
+
);
|
|
85
|
+
return seq;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
global.clearTimeout = function (handle: number): void {
|
|
90
|
+
const activator = getActivator();
|
|
91
|
+
const timerScope = timeoutCancelationScopes.get(handle);
|
|
92
|
+
if (timerScope) {
|
|
93
|
+
timeoutCancelationScopes.delete(handle);
|
|
94
|
+
timerScope.cancel();
|
|
95
|
+
} else {
|
|
96
|
+
activator.nextSeqs.timer++; // Shouldn't increase seq number, but that's the legacy behavior
|
|
97
|
+
activator.completions.timer.delete(handle);
|
|
98
|
+
activator.pushCommand({
|
|
99
|
+
cancelTimer: {
|
|
100
|
+
seq: handle,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// activator.random is mutable, don't hardcode its reference
|
|
107
|
+
Math.random = () => getActivator().random();
|
|
108
|
+
}
|
package/src/interfaces.ts
CHANGED
|
@@ -479,3 +479,8 @@ export type SignalHandlerOptions = { description?: string };
|
|
|
479
479
|
* A validator and description of an update handler.
|
|
480
480
|
*/
|
|
481
481
|
export type UpdateHandlerOptions<Args extends any[]> = { validator?: UpdateValidator<Args>; description?: string };
|
|
482
|
+
|
|
483
|
+
export interface ActivationCompletion {
|
|
484
|
+
commands: coresdk.workflow_commands.IWorkflowCommand[];
|
|
485
|
+
usedInternalFlags: number[];
|
|
486
|
+
}
|
package/src/internals.ts
CHANGED
|
@@ -32,11 +32,13 @@ import {
|
|
|
32
32
|
FileLocation,
|
|
33
33
|
WorkflowInfo,
|
|
34
34
|
WorkflowCreateOptionsInternal,
|
|
35
|
+
ActivationCompletion,
|
|
35
36
|
} from './interfaces';
|
|
36
37
|
import { type SinkCall } from './sinks';
|
|
37
38
|
import { untrackPromise } from './stack-helpers';
|
|
38
39
|
import pkg from './pkg';
|
|
39
40
|
import { executeWithLifecycleLogging } from './logs';
|
|
41
|
+
import { SdkFlag, assertValidFlag } from './flags';
|
|
40
42
|
|
|
41
43
|
enum StartChildWorkflowExecutionFailedCause {
|
|
42
44
|
START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED = 0,
|
|
@@ -304,12 +306,14 @@ export class Activator implements ActivationHandler {
|
|
|
304
306
|
/**
|
|
305
307
|
* Patches we know the status of for this workflow, as in {@link patched}
|
|
306
308
|
*/
|
|
307
|
-
|
|
309
|
+
private readonly knownPresentPatches = new Set<string>();
|
|
308
310
|
|
|
309
311
|
/**
|
|
310
312
|
* Patches we sent to core {@link patched}
|
|
311
313
|
*/
|
|
312
|
-
|
|
314
|
+
private readonly sentPatches = new Set<string>();
|
|
315
|
+
|
|
316
|
+
private readonly knownFlags = new Set<number>();
|
|
313
317
|
|
|
314
318
|
/**
|
|
315
319
|
* Buffered sink calls per activation
|
|
@@ -394,10 +398,11 @@ export class Activator implements ActivationHandler {
|
|
|
394
398
|
}
|
|
395
399
|
}
|
|
396
400
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
+
concludeActivation(): ActivationCompletion {
|
|
402
|
+
return {
|
|
403
|
+
commands: this.commands.splice(0),
|
|
404
|
+
usedInternalFlags: [...this.knownFlags],
|
|
405
|
+
};
|
|
401
406
|
}
|
|
402
407
|
|
|
403
408
|
public async startWorkflowNextHandler({ args }: WorkflowExecuteInput): Promise<any> {
|
|
@@ -780,6 +785,41 @@ export class Activator implements ActivationHandler {
|
|
|
780
785
|
this.knownPresentPatches.add(activation.patchId);
|
|
781
786
|
}
|
|
782
787
|
|
|
788
|
+
public patchInternal(patchId: string, deprecated: boolean): boolean {
|
|
789
|
+
if (this.workflow === undefined) {
|
|
790
|
+
throw new IllegalStateError('Patches cannot be used before Workflow starts');
|
|
791
|
+
}
|
|
792
|
+
const usePatch = !this.info.unsafe.isReplaying || this.knownPresentPatches.has(patchId);
|
|
793
|
+
// Avoid sending commands for patches core already knows about.
|
|
794
|
+
// This optimization enables development of automatic patching tools.
|
|
795
|
+
if (usePatch && !this.sentPatches.has(patchId)) {
|
|
796
|
+
this.pushCommand({
|
|
797
|
+
setPatchMarker: { patchId, deprecated },
|
|
798
|
+
});
|
|
799
|
+
this.sentPatches.add(patchId);
|
|
800
|
+
}
|
|
801
|
+
return usePatch;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Called early while handling an activation to register known flags
|
|
805
|
+
public addKnownFlags(flags: number[]): void {
|
|
806
|
+
for (const flag of flags) {
|
|
807
|
+
assertValidFlag(flag);
|
|
808
|
+
this.knownFlags.add(flag);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
public hasFlag(flag: SdkFlag): boolean {
|
|
813
|
+
if (this.knownFlags.has(flag.id)) {
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
if (!this.info.unsafe.isReplaying && flag.default) {
|
|
817
|
+
this.knownFlags.add(flag.id);
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
783
823
|
public removeFromCache(): void {
|
|
784
824
|
throw new IllegalStateError('removeFromCache activation job should not reach workflow');
|
|
785
825
|
}
|
package/src/trigger.ts
CHANGED
|
@@ -26,7 +26,7 @@ export class Trigger<T> implements PromiseLike<T> {
|
|
|
26
26
|
constructor() {
|
|
27
27
|
this.promise = new Promise<T>((resolve, reject) => {
|
|
28
28
|
const scope = CancellationScope.current();
|
|
29
|
-
if (scope.
|
|
29
|
+
if (scope.cancellable) {
|
|
30
30
|
untrackPromise(scope.cancelRequested.catch(reject));
|
|
31
31
|
}
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
package/src/worker-interface.ts
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { IllegalStateError } from '@temporalio/common';
|
|
7
|
-
import {
|
|
7
|
+
import { tsToMs } from '@temporalio/common/lib/time';
|
|
8
8
|
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
9
9
|
import { coresdk } from '@temporalio/proto';
|
|
10
10
|
import { disableStorage } from './cancellation-scope';
|
|
11
|
-
import { DeterminismViolationError } from './errors';
|
|
12
11
|
import { WorkflowInterceptorsFactory } from './interceptors';
|
|
13
12
|
import { WorkflowCreateOptionsInternal } from './interfaces';
|
|
14
13
|
import { Activator } from './internals';
|
|
@@ -21,72 +20,6 @@ export { PromiseStackStore } from './internals';
|
|
|
21
20
|
const global = globalThis as any;
|
|
22
21
|
const OriginalDate = globalThis.Date;
|
|
23
22
|
|
|
24
|
-
export function overrideGlobals(): void {
|
|
25
|
-
// Mock any weak reference because GC is non-deterministic and the effect is observable from the Workflow.
|
|
26
|
-
// Workflow developer will get a meaningful exception if they try to use these.
|
|
27
|
-
global.WeakRef = function () {
|
|
28
|
-
throw new DeterminismViolationError('WeakRef cannot be used in Workflows because v8 GC is non-deterministic');
|
|
29
|
-
};
|
|
30
|
-
global.FinalizationRegistry = function () {
|
|
31
|
-
throw new DeterminismViolationError(
|
|
32
|
-
'FinalizationRegistry cannot be used in Workflows because v8 GC is non-deterministic'
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
global.Date = function (...args: unknown[]) {
|
|
37
|
-
if (args.length > 0) {
|
|
38
|
-
return new (OriginalDate as any)(...args);
|
|
39
|
-
}
|
|
40
|
-
return new OriginalDate(getActivator().now);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
global.Date.now = function () {
|
|
44
|
-
return getActivator().now;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
global.Date.parse = OriginalDate.parse.bind(OriginalDate);
|
|
48
|
-
global.Date.UTC = OriginalDate.UTC.bind(OriginalDate);
|
|
49
|
-
|
|
50
|
-
global.Date.prototype = OriginalDate.prototype;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param ms sleep duration - number of milliseconds. If given a negative number, value will be set to 1.
|
|
54
|
-
*/
|
|
55
|
-
global.setTimeout = function (cb: (...args: any[]) => any, ms: number, ...args: any[]): number {
|
|
56
|
-
const activator = getActivator();
|
|
57
|
-
ms = Math.max(1, ms);
|
|
58
|
-
const seq = activator.nextSeqs.timer++;
|
|
59
|
-
// Create a Promise for AsyncLocalStorage to be able to track this completion using promise hooks.
|
|
60
|
-
new Promise((resolve, reject) => {
|
|
61
|
-
activator.completions.timer.set(seq, { resolve, reject });
|
|
62
|
-
activator.pushCommand({
|
|
63
|
-
startTimer: {
|
|
64
|
-
seq,
|
|
65
|
-
startToFireTimeout: msToTs(ms),
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}).then(
|
|
69
|
-
() => cb(...args),
|
|
70
|
-
() => undefined /* ignore cancellation */
|
|
71
|
-
);
|
|
72
|
-
return seq;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
global.clearTimeout = function (handle: number): void {
|
|
76
|
-
const activator = getActivator();
|
|
77
|
-
activator.nextSeqs.timer++;
|
|
78
|
-
activator.completions.timer.delete(handle);
|
|
79
|
-
activator.pushCommand({
|
|
80
|
-
cancelTimer: {
|
|
81
|
-
seq: handle,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// activator.random is mutable, don't hardcode its reference
|
|
87
|
-
Math.random = () => getActivator().random();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
23
|
/**
|
|
91
24
|
* Initialize the isolate runtime.
|
|
92
25
|
*
|
|
@@ -190,6 +123,7 @@ export function activate(activation: coresdk.workflow_activation.WorkflowActivat
|
|
|
190
123
|
// timestamp will not be updated for activation that contain only queries
|
|
191
124
|
activator.now = tsToMs(activation.timestamp);
|
|
192
125
|
}
|
|
126
|
+
activator.addKnownFlags(activation.availableInternalFlags ?? []);
|
|
193
127
|
|
|
194
128
|
// The Rust Core ensures that these activation fields are not null
|
|
195
129
|
activator.mutateWorkflowInfo((info) => ({
|
|
@@ -249,11 +183,12 @@ export function concludeActivation(): coresdk.workflow_completion.IWorkflowActiv
|
|
|
249
183
|
activator.rejectBufferedUpdates();
|
|
250
184
|
const intercept = composeInterceptors(activator.interceptors.internals, 'concludeActivation', (input) => input);
|
|
251
185
|
const { info } = activator;
|
|
252
|
-
const
|
|
186
|
+
const activationCompletion = activator.concludeActivation();
|
|
187
|
+
const { commands } = intercept({ commands: activationCompletion.commands });
|
|
253
188
|
|
|
254
189
|
return {
|
|
255
190
|
runId: info.runId,
|
|
256
|
-
successful: { commands },
|
|
191
|
+
successful: { ...activationCompletion, commands },
|
|
257
192
|
};
|
|
258
193
|
}
|
|
259
194
|
|
package/src/workflow.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
ActivityOptions,
|
|
4
4
|
compileRetryPolicy,
|
|
5
5
|
extractWorkflowType,
|
|
6
|
-
IllegalStateError,
|
|
7
6
|
LocalActivityOptions,
|
|
8
7
|
mapToPayloads,
|
|
9
8
|
QueryDefinition,
|
|
@@ -62,7 +61,7 @@ export function addDefaultWorkflowOptions<T extends Workflow>(
|
|
|
62
61
|
const { args, workflowId, ...rest } = opts;
|
|
63
62
|
return {
|
|
64
63
|
workflowId: workflowId ?? uuid4(),
|
|
65
|
-
args: args ?? [],
|
|
64
|
+
args: (args ?? []) as unknown[],
|
|
66
65
|
cancellationType: ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
|
|
67
66
|
...rest,
|
|
68
67
|
};
|
|
@@ -987,7 +986,10 @@ export function uuid4(): string {
|
|
|
987
986
|
* calls with the same ID, which means all such calls will always return the same value.
|
|
988
987
|
*/
|
|
989
988
|
export function patched(patchId: string): boolean {
|
|
990
|
-
|
|
989
|
+
const activator = assertInWorkflowContext(
|
|
990
|
+
'Workflow.patch(...) and Workflow.deprecatePatch may only be used from a Workflow Execution.'
|
|
991
|
+
);
|
|
992
|
+
return activator.patchInternal(patchId, false);
|
|
991
993
|
}
|
|
992
994
|
|
|
993
995
|
/**
|
|
@@ -1008,29 +1010,10 @@ export function patched(patchId: string): boolean {
|
|
|
1008
1010
|
* calls with the same ID, which means all such calls will always return the same value.
|
|
1009
1011
|
*/
|
|
1010
1012
|
export function deprecatePatch(patchId: string): void {
|
|
1011
|
-
patchInternal(patchId, true);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function patchInternal(patchId: string, deprecated: boolean): boolean {
|
|
1015
1013
|
const activator = assertInWorkflowContext(
|
|
1016
1014
|
'Workflow.patch(...) and Workflow.deprecatePatch may only be used from a Workflow Execution.'
|
|
1017
1015
|
);
|
|
1018
|
-
|
|
1019
|
-
// this would be the place to start the interception chain
|
|
1020
|
-
|
|
1021
|
-
if (activator.workflow === undefined) {
|
|
1022
|
-
throw new IllegalStateError('Patches cannot be used before Workflow starts');
|
|
1023
|
-
}
|
|
1024
|
-
const usePatch = !activator.info.unsafe.isReplaying || activator.knownPresentPatches.has(patchId);
|
|
1025
|
-
// Avoid sending commands for patches core already knows about.
|
|
1026
|
-
// This optimization enables development of automatic patching tools.
|
|
1027
|
-
if (usePatch && !activator.sentPatches.has(patchId)) {
|
|
1028
|
-
activator.pushCommand({
|
|
1029
|
-
setPatchMarker: { patchId, deprecated },
|
|
1030
|
-
});
|
|
1031
|
-
activator.sentPatches.add(patchId);
|
|
1032
|
-
}
|
|
1033
|
-
return usePatch;
|
|
1016
|
+
activator.patchInternal(patchId, true);
|
|
1034
1017
|
}
|
|
1035
1018
|
|
|
1036
1019
|
/**
|