@temporalio/workflow 1.0.0-rc.0 → 1.0.1
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/LICENSE.md +1 -1
- package/lib/errors.js +9 -5
- package/lib/errors.js.map +1 -1
- package/lib/index.d.ts +15 -9
- package/lib/index.js +14 -11
- package/lib/index.js.map +1 -1
- package/lib/interfaces.d.ts +73 -5
- package/lib/interfaces.js +44 -0
- package/lib/interfaces.js.map +1 -1
- package/lib/internals.d.ts +0 -8
- package/lib/internals.js +2 -1
- package/lib/internals.js.map +1 -1
- package/lib/sinks.d.ts +2 -1
- package/lib/worker-interface.d.ts +1 -3
- package/lib/worker-interface.js +4 -5
- package/lib/worker-interface.js.map +1 -1
- package/lib/workflow.d.ts +82 -65
- package/lib/workflow.js +56 -48
- package/lib/workflow.js.map +1 -1
- package/package.json +10 -9
- package/src/alea.ts +88 -0
- package/src/cancellation-scope.ts +205 -0
- package/src/errors.ts +30 -0
- package/src/index.ts +91 -0
- package/src/interceptors.ts +239 -0
- package/src/interfaces.ts +294 -0
- package/src/internals.ts +614 -0
- package/src/sinks.ts +41 -0
- package/src/stack-helpers.ts +11 -0
- package/src/trigger.ts +49 -0
- package/src/worker-interface.ts +273 -0
- package/src/workflow-handle.ts +63 -0
- package/src/workflow.ts +1247 -0
package/src/trigger.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CancellationScope } from './cancellation-scope';
|
|
2
|
+
import { untrackPromise } from './stack-helpers';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A `PromiseLike` helper which exposes its `resolve` and `reject` methods.
|
|
6
|
+
*
|
|
7
|
+
* Trigger is CancellationScope-aware: it is linked to the current scope on
|
|
8
|
+
* construction and throws when that scope is cancelled.
|
|
9
|
+
*
|
|
10
|
+
* Useful for e.g. waiting for unblocking a Workflow from a Signal.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <!--SNIPSTART typescript-trigger-workflow-->
|
|
14
|
+
* <!--SNIPEND-->
|
|
15
|
+
*/
|
|
16
|
+
export class Trigger<T> implements PromiseLike<T> {
|
|
17
|
+
// Typescript does not realize that the promise executor is run synchronously in the constructor
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
public readonly resolve: (value: T | PromiseLike<T>) => void;
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
public readonly reject: (reason?: any) => void;
|
|
24
|
+
protected readonly promise: Promise<T>;
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
this.promise = new Promise<T>((resolve, reject) => {
|
|
28
|
+
const scope = CancellationScope.current();
|
|
29
|
+
if (scope.consideredCancelled || scope.cancellable) {
|
|
30
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
31
|
+
}
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
this.resolve = resolve;
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
this.reject = reject;
|
|
38
|
+
});
|
|
39
|
+
// Avoid unhandled rejections
|
|
40
|
+
untrackPromise(this.promise.catch(() => undefined));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
then<TResult1 = T, TResult2 = never>(
|
|
44
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
|
45
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
|
46
|
+
): PromiseLike<TResult1 | TResult2> {
|
|
47
|
+
return this.promise.then(onfulfilled, onrejected);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported functions for the Worker to interact with the Workflow isolate
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { errorToFailure as _errorToFailure, ProtoFailure } from '@temporalio/common';
|
|
7
|
+
import { composeInterceptors, IllegalStateError, msToTs, tsToMs } from '@temporalio/internal-workflow-common';
|
|
8
|
+
import type { coresdk } from '@temporalio/proto';
|
|
9
|
+
import { alea } from './alea';
|
|
10
|
+
import { storage } from './cancellation-scope';
|
|
11
|
+
import { DeterminismViolationError } from './errors';
|
|
12
|
+
import { WorkflowInterceptorsFactory } from './interceptors';
|
|
13
|
+
import { WorkflowInfo } from './interfaces';
|
|
14
|
+
import { InterceptorsImportFunc, state, WorkflowsImportFunc } from './internals';
|
|
15
|
+
import { SinkCall } from './sinks';
|
|
16
|
+
|
|
17
|
+
// Export the type for use on the "worker" side
|
|
18
|
+
export { PromiseStackStore } from './internals';
|
|
19
|
+
|
|
20
|
+
export interface WorkflowCreateOptions {
|
|
21
|
+
info: WorkflowInfo;
|
|
22
|
+
randomnessSeed: number[];
|
|
23
|
+
now: number;
|
|
24
|
+
patches: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ImportFunctions {
|
|
28
|
+
importWorkflows: WorkflowsImportFunc;
|
|
29
|
+
importInterceptors: InterceptorsImportFunc;
|
|
30
|
+
}
|
|
31
|
+
export function setImportFuncs({ importWorkflows, importInterceptors }: ImportFunctions): void {
|
|
32
|
+
state.importWorkflows = importWorkflows;
|
|
33
|
+
state.importInterceptors = importInterceptors;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function overrideGlobals(): void {
|
|
37
|
+
const global = globalThis as any;
|
|
38
|
+
// Mock any weak reference because GC is non-deterministic and the effect is observable from the Workflow.
|
|
39
|
+
// WeakRef is implemented in V8 8.4 which is embedded in node >=14.6.0.
|
|
40
|
+
// Workflow developer will get a meaningful exception if they try to use these.
|
|
41
|
+
global.WeakRef = function () {
|
|
42
|
+
throw new DeterminismViolationError('WeakRef cannot be used in Workflows because v8 GC is non-deterministic');
|
|
43
|
+
};
|
|
44
|
+
global.FinalizationRegistry = function () {
|
|
45
|
+
throw new DeterminismViolationError(
|
|
46
|
+
'FinalizationRegistry cannot be used in Workflows because v8 GC is non-deterministic'
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const OriginalDate = globalThis.Date;
|
|
51
|
+
|
|
52
|
+
global.Date = function (...args: unknown[]) {
|
|
53
|
+
if (args.length > 0) {
|
|
54
|
+
return new (OriginalDate as any)(...args);
|
|
55
|
+
}
|
|
56
|
+
return new OriginalDate(state.now);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
global.Date.now = function () {
|
|
60
|
+
return state.now;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
global.Date.parse = OriginalDate.parse.bind(OriginalDate);
|
|
64
|
+
global.Date.UTC = OriginalDate.UTC.bind(OriginalDate);
|
|
65
|
+
|
|
66
|
+
global.Date.prototype = OriginalDate.prototype;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param ms sleep duration - number of milliseconds. If given a negative number, value will be set to 1.
|
|
70
|
+
*/
|
|
71
|
+
global.setTimeout = function (cb: (...args: any[]) => any, ms: number, ...args: any[]): number {
|
|
72
|
+
ms = Math.max(1, ms);
|
|
73
|
+
const seq = state.nextSeqs.timer++;
|
|
74
|
+
// Create a Promise for AsyncLocalStorage to be able to track this completion using promise hooks.
|
|
75
|
+
new Promise((resolve, reject) => {
|
|
76
|
+
state.completions.timer.set(seq, { resolve, reject });
|
|
77
|
+
state.pushCommand({
|
|
78
|
+
startTimer: {
|
|
79
|
+
seq,
|
|
80
|
+
startToFireTimeout: msToTs(ms),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}).then(
|
|
84
|
+
() => cb(...args),
|
|
85
|
+
() => undefined /* ignore cancellation */
|
|
86
|
+
);
|
|
87
|
+
return seq;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
global.clearTimeout = function (handle: number): void {
|
|
91
|
+
state.nextSeqs.timer++;
|
|
92
|
+
state.completions.timer.delete(handle);
|
|
93
|
+
state.pushCommand({
|
|
94
|
+
cancelTimer: {
|
|
95
|
+
seq: handle,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// state.random is mutable, don't hardcode its reference
|
|
101
|
+
Math.random = () => state.random();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the isolate runtime.
|
|
106
|
+
*
|
|
107
|
+
* Sets required internal state and instantiates the workflow and interceptors.
|
|
108
|
+
*/
|
|
109
|
+
export async function initRuntime({ info, randomnessSeed, now, patches }: WorkflowCreateOptions): Promise<void> {
|
|
110
|
+
const global = globalThis as any;
|
|
111
|
+
// Set the runId globally on the context so it can be retrieved in the case
|
|
112
|
+
// of an unhandled promise rejection.
|
|
113
|
+
global.__TEMPORAL__.runId = info.runId;
|
|
114
|
+
// Set the promiseStackStore so promises can be tracked
|
|
115
|
+
global.__TEMPORAL__.promiseStackStore = {
|
|
116
|
+
promiseToStack: new Map(),
|
|
117
|
+
childToParent: new Map(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
state.info = info;
|
|
121
|
+
state.now = now;
|
|
122
|
+
state.random = alea(randomnessSeed);
|
|
123
|
+
|
|
124
|
+
if (info.unsafe.isReplaying) {
|
|
125
|
+
for (const patch of patches) {
|
|
126
|
+
state.knownPresentPatches.add(patch);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// webpack doesn't know what to bundle given a dynamic import expression, so we can't do:
|
|
131
|
+
// state.payloadConverter = (await import(payloadConverterPath)).payloadConverter;
|
|
132
|
+
// @ts-expect-error this is a webpack alias to payloadConverterPath
|
|
133
|
+
const customPayloadConverter = (await import('__temporal_custom_payload_converter')).payloadConverter;
|
|
134
|
+
// The `payloadConverter` export is validated in the Worker
|
|
135
|
+
if (customPayloadConverter !== undefined) {
|
|
136
|
+
state.payloadConverter = customPayloadConverter;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { importWorkflows, importInterceptors } = state;
|
|
140
|
+
if (importWorkflows === undefined || importInterceptors === undefined) {
|
|
141
|
+
throw new IllegalStateError('Workflow has not been initialized');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const interceptors = await importInterceptors();
|
|
145
|
+
for (const mod of interceptors) {
|
|
146
|
+
const factory: WorkflowInterceptorsFactory = mod.interceptors;
|
|
147
|
+
if (factory !== undefined) {
|
|
148
|
+
if (typeof factory !== 'function') {
|
|
149
|
+
throw new TypeError(`interceptors must be a function, got: ${factory}`);
|
|
150
|
+
}
|
|
151
|
+
const interceptors = factory();
|
|
152
|
+
state.interceptors.inbound.push(...(interceptors.inbound ?? []));
|
|
153
|
+
state.interceptors.outbound.push(...(interceptors.outbound ?? []));
|
|
154
|
+
state.interceptors.internals.push(...(interceptors.internals ?? []));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const mod = await importWorkflows();
|
|
159
|
+
const workflow = mod[info.workflowType];
|
|
160
|
+
if (typeof workflow !== 'function') {
|
|
161
|
+
throw new TypeError(`'${info.workflowType}' is not a function`);
|
|
162
|
+
}
|
|
163
|
+
state.workflow = workflow;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run a chunk of activation jobs
|
|
168
|
+
* @returns a boolean indicating whether job was processed or ignored
|
|
169
|
+
*/
|
|
170
|
+
export function activate(activation: coresdk.workflow_activation.WorkflowActivation, batchIndex: number): void {
|
|
171
|
+
const intercept = composeInterceptors(state.interceptors.internals, 'activate', ({ activation, batchIndex }) => {
|
|
172
|
+
if (batchIndex === 0) {
|
|
173
|
+
if (state.info === undefined) {
|
|
174
|
+
throw new IllegalStateError('Workflow has not been initialized');
|
|
175
|
+
}
|
|
176
|
+
if (!activation.jobs) {
|
|
177
|
+
throw new TypeError('Got activation with no jobs');
|
|
178
|
+
}
|
|
179
|
+
if (activation.timestamp != null) {
|
|
180
|
+
// timestamp will not be updated for activation that contain only queries
|
|
181
|
+
state.now = tsToMs(activation.timestamp);
|
|
182
|
+
}
|
|
183
|
+
if (activation.historyLength == null) {
|
|
184
|
+
throw new TypeError('Got activation with no historyLength');
|
|
185
|
+
}
|
|
186
|
+
state.info.unsafe.isReplaying = activation.isReplaying ?? false;
|
|
187
|
+
state.info.historyLength = activation.historyLength;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Cast from the interface to the class which has the `variant` attribute.
|
|
191
|
+
// This is safe because we know that activation is a proto class.
|
|
192
|
+
const jobs = activation.jobs as coresdk.workflow_activation.WorkflowActivationJob[];
|
|
193
|
+
|
|
194
|
+
for (const job of jobs) {
|
|
195
|
+
if (job.variant === undefined) {
|
|
196
|
+
throw new TypeError('Expected job.variant to be defined');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const variant = job[job.variant];
|
|
200
|
+
if (!variant) {
|
|
201
|
+
throw new TypeError(`Expected job.${job.variant} to be set`);
|
|
202
|
+
}
|
|
203
|
+
// The only job that can be executed on a completed workflow is a query.
|
|
204
|
+
// We might get other jobs after completion for instance when a single
|
|
205
|
+
// activation contains multiple jobs and the first one completes the workflow.
|
|
206
|
+
if (state.completed && job.variant !== 'queryWorkflow') {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
state.activator[job.variant](variant as any /* TS can't infer this type */);
|
|
210
|
+
tryUnblockConditions();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
intercept({
|
|
214
|
+
activation,
|
|
215
|
+
batchIndex,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Conclude a single activation.
|
|
221
|
+
* Should be called after processing all activation jobs and queued microtasks.
|
|
222
|
+
*
|
|
223
|
+
* Activation failures are handled in the main Node.js isolate.
|
|
224
|
+
*/
|
|
225
|
+
export function concludeActivation(): coresdk.workflow_completion.IWorkflowActivationCompletion {
|
|
226
|
+
const intercept = composeInterceptors(state.interceptors.internals, 'concludeActivation', (input) => input);
|
|
227
|
+
const { info } = state;
|
|
228
|
+
const { commands } = intercept({ commands: state.commands });
|
|
229
|
+
state.commands = [];
|
|
230
|
+
return {
|
|
231
|
+
runId: info?.runId,
|
|
232
|
+
successful: { commands },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getAndResetSinkCalls(): SinkCall[] {
|
|
237
|
+
return state.getAndResetSinkCalls();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Loop through all blocked conditions, evaluate and unblock if possible.
|
|
242
|
+
*
|
|
243
|
+
* @returns number of unblocked conditions.
|
|
244
|
+
*/
|
|
245
|
+
export function tryUnblockConditions(): number {
|
|
246
|
+
let numUnblocked = 0;
|
|
247
|
+
for (;;) {
|
|
248
|
+
const prevUnblocked = numUnblocked;
|
|
249
|
+
for (const [seq, cond] of state.blockedConditions.entries()) {
|
|
250
|
+
if (cond.fn()) {
|
|
251
|
+
cond.resolve();
|
|
252
|
+
numUnblocked++;
|
|
253
|
+
// It is safe to delete elements during map iteration
|
|
254
|
+
state.blockedConditions.delete(seq);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (prevUnblocked === numUnblocked) {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return numUnblocked;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function dispose(): Promise<void> {
|
|
265
|
+
const dispose = composeInterceptors(state.interceptors.internals, 'dispose', async () => {
|
|
266
|
+
storage.disable();
|
|
267
|
+
});
|
|
268
|
+
await dispose({});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function errorToFailure(err: unknown): ProtoFailure {
|
|
272
|
+
return _errorToFailure(err, state.payloadConverter);
|
|
273
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BaseWorkflowHandle, SignalDefinition, Workflow } from '@temporalio/internal-workflow-common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle representing an external Workflow Execution.
|
|
5
|
+
*
|
|
6
|
+
* This handle only has methods `cancel` and `signal`. To call other methods, like `query` and `result`, use
|
|
7
|
+
* {@link WorkflowClient.getHandle} inside an Activity.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExternalWorkflowHandle {
|
|
10
|
+
/**
|
|
11
|
+
* Signal a running Workflow.
|
|
12
|
+
*
|
|
13
|
+
* @param def a signal definition as returned from {@link defineSignal} or signal name (string)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* await handle.signal(incrementSignal, 3);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
signal<Args extends any[] = []>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cancel the external Workflow execution.
|
|
24
|
+
*
|
|
25
|
+
* Throws if the Workflow execution does not exist.
|
|
26
|
+
*/
|
|
27
|
+
cancel(): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The workflowId of the external Workflow
|
|
31
|
+
*/
|
|
32
|
+
readonly workflowId: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* An optional runId of the external Workflow
|
|
36
|
+
*/
|
|
37
|
+
readonly runId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A client side handle to a single Workflow instance.
|
|
42
|
+
* It can be used to signal, wait for completion, and cancel a Workflow execution.
|
|
43
|
+
*
|
|
44
|
+
* Given the following Workflow definition:
|
|
45
|
+
* ```ts
|
|
46
|
+
* export const incrementSignal = defineSignal('increment');
|
|
47
|
+
* export async function counterWorkflow(initialValue: number): Promise<void>;
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Start a new Workflow execution and get a handle for interacting with it:
|
|
51
|
+
* ```ts
|
|
52
|
+
* // Start the Workflow with initialValue of 2.
|
|
53
|
+
* const handle = await startWorkflow(counterWorkflow, { args: [2] });
|
|
54
|
+
* await handle.signal(incrementSignal, 2);
|
|
55
|
+
* await handle.result(); // throws WorkflowExecutionTerminatedError
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export interface ChildWorkflowHandle<T extends Workflow> extends BaseWorkflowHandle<T> {
|
|
59
|
+
/**
|
|
60
|
+
* The runId of the initial run of the bound Workflow
|
|
61
|
+
*/
|
|
62
|
+
readonly firstExecutionRunId: string;
|
|
63
|
+
}
|