@temporalio/workflow 0.22.0 → 1.0.0-rc.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/README.md +2 -2
- package/lib/cancellation-scope.js +6 -2
- package/lib/cancellation-scope.js.map +1 -1
- package/lib/errors.js +9 -5
- package/lib/errors.js.map +1 -1
- package/lib/index.d.ts +18 -12
- package/lib/index.js +16 -13
- package/lib/index.js.map +1 -1
- package/lib/interceptors.d.ts +3 -1
- package/lib/interfaces.d.ts +129 -13
- package/lib/interfaces.js +44 -0
- package/lib/interfaces.js.map +1 -1
- package/lib/internals.d.ts +17 -2
- package/lib/internals.js +45 -12
- package/lib/internals.js.map +1 -1
- package/lib/sinks.d.ts +2 -1
- package/lib/stack-helpers.d.ts +4 -0
- package/lib/stack-helpers.js +15 -0
- package/lib/stack-helpers.js.map +1 -0
- package/lib/trigger.d.ts +5 -4
- package/lib/trigger.js +10 -7
- package/lib/trigger.js.map +1 -1
- package/lib/worker-interface.d.ts +5 -2
- package/lib/worker-interface.js +17 -11
- package/lib/worker-interface.js.map +1 -1
- package/lib/workflow-handle.d.ts +5 -2
- package/lib/workflow.d.ts +120 -42
- package/lib/workflow.js +164 -78
- package/lib/workflow.js.map +1 -1
- package/package.json +9 -7
- 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 +272 -0
- package/src/internals.ts +621 -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 +283 -0
- package/src/workflow-handle.ts +63 -0
- package/src/workflow.ts +1265 -0
package/src/internals.ts
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { PayloadConverter } from '@temporalio/common';
|
|
2
|
+
import {
|
|
3
|
+
arrayFromPayloads,
|
|
4
|
+
defaultPayloadConverter,
|
|
5
|
+
ensureTemporalFailure,
|
|
6
|
+
errorToFailure,
|
|
7
|
+
failureToError,
|
|
8
|
+
optionalFailureToOptionalError,
|
|
9
|
+
TemporalFailure,
|
|
10
|
+
} from '@temporalio/common';
|
|
11
|
+
import {
|
|
12
|
+
checkExtends,
|
|
13
|
+
composeInterceptors,
|
|
14
|
+
IllegalStateError,
|
|
15
|
+
Workflow,
|
|
16
|
+
WorkflowQueryType,
|
|
17
|
+
WorkflowSignalType,
|
|
18
|
+
} from '@temporalio/internal-workflow-common';
|
|
19
|
+
import type { coresdk } from '@temporalio/proto';
|
|
20
|
+
import { alea, RNG } from './alea';
|
|
21
|
+
import { ROOT_SCOPE } from './cancellation-scope';
|
|
22
|
+
import { DeterminismViolationError, isCancellation, WorkflowExecutionAlreadyStartedError } from './errors';
|
|
23
|
+
import {
|
|
24
|
+
QueryInput,
|
|
25
|
+
SignalInput,
|
|
26
|
+
WorkflowExecuteInput,
|
|
27
|
+
WorkflowInterceptors,
|
|
28
|
+
WorkflowInterceptorsFactory,
|
|
29
|
+
} from './interceptors';
|
|
30
|
+
import { ContinueAsNew, WorkflowInfo } from './interfaces';
|
|
31
|
+
import { SinkCall } from './sinks';
|
|
32
|
+
import { untrackPromise } from './stack-helpers';
|
|
33
|
+
|
|
34
|
+
enum StartChildWorkflowExecutionFailedCause {
|
|
35
|
+
START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED = 0,
|
|
36
|
+
START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS = 1,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
checkExtends<coresdk.child_workflow.StartChildWorkflowExecutionFailedCause, StartChildWorkflowExecutionFailedCause>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Global store to track promise stacks for stack trace query
|
|
43
|
+
*/
|
|
44
|
+
export interface PromiseStackStore {
|
|
45
|
+
childToParent: Map<Promise<unknown>, Set<Promise<unknown>>>;
|
|
46
|
+
promiseToStack: Map<Promise<unknown>, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ResolveFunction<T = any> = (val: T) => any;
|
|
50
|
+
export type RejectFunction<E = any> = (val: E) => any;
|
|
51
|
+
|
|
52
|
+
export interface Completion {
|
|
53
|
+
resolve: ResolveFunction;
|
|
54
|
+
reject: RejectFunction;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Condition {
|
|
58
|
+
fn(): boolean;
|
|
59
|
+
resolve(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A class that acts as a marker for this special result type
|
|
64
|
+
*/
|
|
65
|
+
export class LocalActivityDoBackoff {
|
|
66
|
+
public readonly name = 'LocalActivityDoBackoff';
|
|
67
|
+
constructor(public readonly backoff: coresdk.activity_result.IDoBackoff) {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ActivationHandlerFunction<K extends keyof coresdk.workflow_activation.IWorkflowActivationJob> = (
|
|
71
|
+
activation: NonNullable<coresdk.workflow_activation.IWorkflowActivationJob[K]>
|
|
72
|
+
) => void;
|
|
73
|
+
|
|
74
|
+
export type ActivationHandler = {
|
|
75
|
+
[P in keyof coresdk.workflow_activation.IWorkflowActivationJob]: ActivationHandlerFunction<P>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export class Activator implements ActivationHandler {
|
|
79
|
+
workflowFunctionWasCalled = false;
|
|
80
|
+
|
|
81
|
+
public async startWorkflowNextHandler({ args }: WorkflowExecuteInput): Promise<any> {
|
|
82
|
+
const { workflow } = state;
|
|
83
|
+
if (workflow === undefined) {
|
|
84
|
+
throw new IllegalStateError('Workflow uninitialized');
|
|
85
|
+
}
|
|
86
|
+
let promise: Promise<any>;
|
|
87
|
+
try {
|
|
88
|
+
promise = workflow(...args);
|
|
89
|
+
} finally {
|
|
90
|
+
// Guarantee this runs even if there was an exception when invoking the Workflow function
|
|
91
|
+
// Otherwise this Workflow will now be queryable.
|
|
92
|
+
this.workflowFunctionWasCalled = true;
|
|
93
|
+
// Empty the buffer
|
|
94
|
+
const buffer = state.bufferedQueries.splice(0);
|
|
95
|
+
for (const activation of buffer) {
|
|
96
|
+
this.queryWorkflow(activation);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return await promise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public startWorkflow(activation: coresdk.workflow_activation.IStartWorkflow): void {
|
|
103
|
+
const { info } = state;
|
|
104
|
+
if (info === undefined) {
|
|
105
|
+
throw new IllegalStateError('Workflow has not been initialized');
|
|
106
|
+
}
|
|
107
|
+
const execute = composeInterceptors(
|
|
108
|
+
state.interceptors.inbound,
|
|
109
|
+
'execute',
|
|
110
|
+
this.startWorkflowNextHandler.bind(this)
|
|
111
|
+
);
|
|
112
|
+
untrackPromise(
|
|
113
|
+
execute({
|
|
114
|
+
headers: activation.headers ?? {},
|
|
115
|
+
args: arrayFromPayloads(state.payloadConverter, activation.arguments),
|
|
116
|
+
}).then(completeWorkflow, handleWorkflowFailure)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public cancelWorkflow(_activation: coresdk.workflow_activation.ICancelWorkflow): void {
|
|
121
|
+
state.cancelled = true;
|
|
122
|
+
ROOT_SCOPE.cancel();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public fireTimer(activation: coresdk.workflow_activation.IFireTimer): void {
|
|
126
|
+
// Timers are a special case where their completion might not be in Workflow state,
|
|
127
|
+
// this is due to immediate timer cancellation that doesn't go wait for Core.
|
|
128
|
+
const completion = maybeConsumeCompletion('timer', getSeq(activation));
|
|
129
|
+
completion?.resolve(undefined);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public resolveActivity(activation: coresdk.workflow_activation.IResolveActivity): void {
|
|
133
|
+
if (!activation.result) {
|
|
134
|
+
throw new TypeError('Got ResolveActivity activation with no result');
|
|
135
|
+
}
|
|
136
|
+
const { resolve, reject } = consumeCompletion('activity', getSeq(activation));
|
|
137
|
+
if (activation.result.completed) {
|
|
138
|
+
const completed = activation.result.completed;
|
|
139
|
+
const result = completed.result ? state.payloadConverter.fromPayload(completed.result) : undefined;
|
|
140
|
+
resolve(result);
|
|
141
|
+
} else if (activation.result.failed) {
|
|
142
|
+
const { failure } = activation.result.failed;
|
|
143
|
+
const err = optionalFailureToOptionalError(failure, state.payloadConverter);
|
|
144
|
+
reject(err);
|
|
145
|
+
} else if (activation.result.cancelled) {
|
|
146
|
+
const { failure } = activation.result.cancelled;
|
|
147
|
+
const err = optionalFailureToOptionalError(failure, state.payloadConverter);
|
|
148
|
+
reject(err);
|
|
149
|
+
} else if (activation.result.backoff) {
|
|
150
|
+
reject(new LocalActivityDoBackoff(activation.result.backoff));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public resolveChildWorkflowExecutionStart(
|
|
155
|
+
activation: coresdk.workflow_activation.IResolveChildWorkflowExecutionStart
|
|
156
|
+
): void {
|
|
157
|
+
const { resolve, reject } = consumeCompletion('childWorkflowStart', getSeq(activation));
|
|
158
|
+
if (activation.succeeded) {
|
|
159
|
+
resolve(activation.succeeded.runId);
|
|
160
|
+
} else if (activation.failed) {
|
|
161
|
+
if (
|
|
162
|
+
activation.failed.cause !==
|
|
163
|
+
StartChildWorkflowExecutionFailedCause.START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS
|
|
164
|
+
) {
|
|
165
|
+
throw new IllegalStateError('Got unknown StartChildWorkflowExecutionFailedCause');
|
|
166
|
+
}
|
|
167
|
+
if (!(activation.seq && activation.failed.workflowId && activation.failed.workflowType)) {
|
|
168
|
+
throw new TypeError('Missing attributes in activation job');
|
|
169
|
+
}
|
|
170
|
+
reject(
|
|
171
|
+
new WorkflowExecutionAlreadyStartedError(
|
|
172
|
+
'Workflow execution already started',
|
|
173
|
+
activation.failed.workflowId,
|
|
174
|
+
activation.failed.workflowType
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
} else if (activation.cancelled) {
|
|
178
|
+
if (!activation.cancelled.failure) {
|
|
179
|
+
throw new TypeError('Got no failure in cancelled variant');
|
|
180
|
+
}
|
|
181
|
+
reject(failureToError(activation.cancelled.failure, state.payloadConverter));
|
|
182
|
+
} else {
|
|
183
|
+
throw new TypeError('Got ResolveChildWorkflowExecutionStart with no status');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public resolveChildWorkflowExecution(activation: coresdk.workflow_activation.IResolveChildWorkflowExecution): void {
|
|
188
|
+
if (!activation.result) {
|
|
189
|
+
throw new TypeError('Got ResolveChildWorkflowExecution activation with no result');
|
|
190
|
+
}
|
|
191
|
+
const { resolve, reject } = consumeCompletion('childWorkflowComplete', getSeq(activation));
|
|
192
|
+
if (activation.result.completed) {
|
|
193
|
+
const completed = activation.result.completed;
|
|
194
|
+
const result = completed.result ? state.payloadConverter.fromPayload(completed.result) : undefined;
|
|
195
|
+
resolve(result);
|
|
196
|
+
} else if (activation.result.failed) {
|
|
197
|
+
const { failure } = activation.result.failed;
|
|
198
|
+
if (failure === undefined || failure === null) {
|
|
199
|
+
throw new TypeError('Got failed result with no failure attribute');
|
|
200
|
+
}
|
|
201
|
+
reject(failureToError(failure, state.payloadConverter));
|
|
202
|
+
} else if (activation.result.cancelled) {
|
|
203
|
+
const { failure } = activation.result.cancelled;
|
|
204
|
+
if (failure === undefined || failure === null) {
|
|
205
|
+
throw new TypeError('Got cancelled result with no failure attribute');
|
|
206
|
+
}
|
|
207
|
+
reject(failureToError(failure, state.payloadConverter));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Intentionally not made function async so this handler doesn't show up in the stack trace
|
|
212
|
+
protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
|
|
213
|
+
const fn = state.queryHandlers.get(queryName);
|
|
214
|
+
if (fn === undefined) {
|
|
215
|
+
// Fail the query
|
|
216
|
+
throw new ReferenceError(`Workflow did not register a handler for ${queryName}`);
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const ret = fn(...args);
|
|
220
|
+
if (ret instanceof Promise) {
|
|
221
|
+
return Promise.reject(new DeterminismViolationError('Query handlers should not return a Promise'));
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve(ret);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return Promise.reject(err);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public queryWorkflow(activation: coresdk.workflow_activation.IQueryWorkflow): void {
|
|
230
|
+
if (!this.workflowFunctionWasCalled) {
|
|
231
|
+
state.bufferedQueries.push(activation);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { queryType, queryId, headers } = activation;
|
|
236
|
+
if (!(queryType && queryId)) {
|
|
237
|
+
throw new TypeError('Missing query activation attributes');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const execute = composeInterceptors(
|
|
241
|
+
state.interceptors.inbound,
|
|
242
|
+
'handleQuery',
|
|
243
|
+
this.queryWorkflowNextHandler.bind(this)
|
|
244
|
+
);
|
|
245
|
+
execute({
|
|
246
|
+
queryName: queryType,
|
|
247
|
+
args: arrayFromPayloads(state.payloadConverter, activation.arguments),
|
|
248
|
+
queryId,
|
|
249
|
+
headers: headers ?? {},
|
|
250
|
+
}).then(
|
|
251
|
+
(result) => completeQuery(queryId, result),
|
|
252
|
+
(reason) => failQuery(queryId, reason)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public async signalWorkflowNextHandler({ signalName, args }: SignalInput): Promise<void> {
|
|
257
|
+
const fn = state.signalHandlers.get(signalName);
|
|
258
|
+
if (fn === undefined) {
|
|
259
|
+
throw new IllegalStateError(`No registered signal handler for signal ${signalName}`);
|
|
260
|
+
}
|
|
261
|
+
return await fn(...args);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public signalWorkflow(activation: coresdk.workflow_activation.ISignalWorkflow): void {
|
|
265
|
+
const { signalName, headers } = activation;
|
|
266
|
+
if (!signalName) {
|
|
267
|
+
throw new TypeError('Missing activation signalName');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const fn = state.signalHandlers.get(signalName);
|
|
271
|
+
if (fn === undefined) {
|
|
272
|
+
let buffer = state.bufferedSignals.get(signalName);
|
|
273
|
+
if (buffer === undefined) {
|
|
274
|
+
buffer = [];
|
|
275
|
+
state.bufferedSignals.set(signalName, buffer);
|
|
276
|
+
}
|
|
277
|
+
buffer.push(activation);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const execute = composeInterceptors(
|
|
282
|
+
state.interceptors.inbound,
|
|
283
|
+
'handleSignal',
|
|
284
|
+
this.signalWorkflowNextHandler.bind(this)
|
|
285
|
+
);
|
|
286
|
+
execute({
|
|
287
|
+
args: arrayFromPayloads(state.payloadConverter, activation.input),
|
|
288
|
+
signalName,
|
|
289
|
+
headers: headers ?? {},
|
|
290
|
+
}).catch(handleWorkflowFailure);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public resolveSignalExternalWorkflow(activation: coresdk.workflow_activation.IResolveSignalExternalWorkflow): void {
|
|
294
|
+
const { resolve, reject } = consumeCompletion('signalWorkflow', getSeq(activation));
|
|
295
|
+
if (activation.failure) {
|
|
296
|
+
reject(failureToError(activation.failure, state.payloadConverter));
|
|
297
|
+
} else {
|
|
298
|
+
resolve(undefined);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
public resolveRequestCancelExternalWorkflow(
|
|
303
|
+
activation: coresdk.workflow_activation.IResolveRequestCancelExternalWorkflow
|
|
304
|
+
): void {
|
|
305
|
+
const { resolve, reject } = consumeCompletion('cancelWorkflow', getSeq(activation));
|
|
306
|
+
if (activation.failure) {
|
|
307
|
+
reject(failureToError(activation.failure, state.payloadConverter));
|
|
308
|
+
} else {
|
|
309
|
+
resolve(undefined);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public updateRandomSeed(activation: coresdk.workflow_activation.IUpdateRandomSeed): void {
|
|
314
|
+
if (!activation.randomnessSeed) {
|
|
315
|
+
throw new TypeError('Expected activation with randomnessSeed attribute');
|
|
316
|
+
}
|
|
317
|
+
state.random = alea(activation.randomnessSeed.toBytes());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public notifyHasPatch(activation: coresdk.workflow_activation.INotifyHasPatch): void {
|
|
321
|
+
if (!activation.patchId) {
|
|
322
|
+
throw new TypeError('Notify has patch missing patch name');
|
|
323
|
+
}
|
|
324
|
+
state.knownPresentPatches.add(activation.patchId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public removeFromCache(): void {
|
|
328
|
+
throw new IllegalStateError('removeFromCache activation job should not reach workflow');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export type WorkflowsImportFunc = () => Promise<Record<string, any>>;
|
|
333
|
+
export type InterceptorsImportFunc = () => Promise<Array<{ interceptors: WorkflowInterceptorsFactory }>>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Keeps all of the Workflow runtime state like pending completions for activities and timers and the scope stack.
|
|
337
|
+
*
|
|
338
|
+
* State mutates each time the Workflow is activated.
|
|
339
|
+
*/
|
|
340
|
+
export class State {
|
|
341
|
+
/**
|
|
342
|
+
* Activator executes activation jobs
|
|
343
|
+
*/
|
|
344
|
+
public readonly activator = new Activator();
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Map of task sequence to a Completion
|
|
348
|
+
*/
|
|
349
|
+
public readonly completions = {
|
|
350
|
+
timer: new Map<number, Completion>(),
|
|
351
|
+
activity: new Map<number, Completion>(),
|
|
352
|
+
childWorkflowStart: new Map<number, Completion>(),
|
|
353
|
+
childWorkflowComplete: new Map<number, Completion>(),
|
|
354
|
+
signalWorkflow: new Map<number, Completion>(),
|
|
355
|
+
cancelWorkflow: new Map<number, Completion>(),
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Holds buffered signal calls until a handler is registered
|
|
360
|
+
*/
|
|
361
|
+
public readonly bufferedSignals = new Map<string, coresdk.workflow_activation.ISignalWorkflow[]>();
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Holds buffered query calls until a handler is registered.
|
|
365
|
+
*
|
|
366
|
+
* **IMPORTANT** queries are only buffered until workflow is started.
|
|
367
|
+
* This is required because async interceptors might block workflow function invocation
|
|
368
|
+
* which delays query handler registration.
|
|
369
|
+
*/
|
|
370
|
+
public readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Mapping of signal name to handler
|
|
374
|
+
*/
|
|
375
|
+
public readonly signalHandlers = new Map<string, WorkflowSignalType>();
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Mapping of query name to handler
|
|
379
|
+
*/
|
|
380
|
+
public readonly queryHandlers = new Map<string, WorkflowQueryType>([
|
|
381
|
+
[
|
|
382
|
+
'__stack_trace',
|
|
383
|
+
() => {
|
|
384
|
+
const { childToParent, promiseToStack } = (globalThis as any).__TEMPORAL__
|
|
385
|
+
.promiseStackStore as PromiseStackStore;
|
|
386
|
+
const internalNodes = new Set(
|
|
387
|
+
[...childToParent.values()].reduce((acc, curr) => {
|
|
388
|
+
for (const p of curr) {
|
|
389
|
+
acc.add(p);
|
|
390
|
+
}
|
|
391
|
+
return acc;
|
|
392
|
+
}, new Set())
|
|
393
|
+
);
|
|
394
|
+
const stacks = new Set<string>();
|
|
395
|
+
for (const child of childToParent.keys()) {
|
|
396
|
+
if (!internalNodes.has(child)) {
|
|
397
|
+
const stack = promiseToStack.get(child);
|
|
398
|
+
if (!stack) continue;
|
|
399
|
+
stacks.add(stack);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Not 100% sure where this comes from, just filter it out
|
|
403
|
+
stacks.delete(' at Promise.then (<anonymous>)');
|
|
404
|
+
stacks.delete(' at Promise.then (<anonymous>)\n');
|
|
405
|
+
return [...stacks].join('\n\n');
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Loaded in {@link initRuntime}
|
|
412
|
+
*/
|
|
413
|
+
public interceptors: Required<WorkflowInterceptors> = { inbound: [], outbound: [], internals: [] };
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Buffer that stores all generated commands, reset after each activation
|
|
417
|
+
*/
|
|
418
|
+
public commands: coresdk.workflow_commands.IWorkflowCommand[] = [];
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Stores all {@link condition}s that haven't been unblocked yet
|
|
422
|
+
*/
|
|
423
|
+
public blockedConditions = new Map<number, Condition>();
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Is this Workflow completed?
|
|
427
|
+
*
|
|
428
|
+
* A Workflow will be considered completed if it generates a command that the
|
|
429
|
+
* system considers as a final Workflow command (e.g.
|
|
430
|
+
* completeWorkflowExecution or failWorkflowExecution).
|
|
431
|
+
*/
|
|
432
|
+
public completed = false;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Was this Workflow cancelled?
|
|
436
|
+
*/
|
|
437
|
+
public cancelled = false;
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The next (incremental) sequence to assign when generating completable commands
|
|
441
|
+
*/
|
|
442
|
+
public nextSeqs = {
|
|
443
|
+
timer: 1,
|
|
444
|
+
activity: 1,
|
|
445
|
+
childWorkflow: 1,
|
|
446
|
+
signalWorkflow: 1,
|
|
447
|
+
cancelWorkflow: 1,
|
|
448
|
+
condition: 1,
|
|
449
|
+
// Used internally to keep track of active stack traces
|
|
450
|
+
stack: 1,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* This is set every time the workflow executes an activation
|
|
455
|
+
*/
|
|
456
|
+
#now: number | undefined;
|
|
457
|
+
|
|
458
|
+
get now(): number {
|
|
459
|
+
if (this.#now === undefined) {
|
|
460
|
+
throw new IllegalStateError('Tried to get Date before Workflow has been initialized');
|
|
461
|
+
}
|
|
462
|
+
return this.#now;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
set now(value: number) {
|
|
466
|
+
this.#now = value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Reference to the current Workflow, initialized when a Workflow is started
|
|
471
|
+
*/
|
|
472
|
+
public workflow?: Workflow;
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Information about the current Workflow
|
|
476
|
+
*/
|
|
477
|
+
public info?: WorkflowInfo;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Whether a Workflow is replaying history or processing new events
|
|
481
|
+
*/
|
|
482
|
+
isReplaying?: boolean;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* ID of last WorkflowTaskStarted event
|
|
486
|
+
*/
|
|
487
|
+
historyLength?: number;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* A deterministic RNG, used by the isolate's overridden Math.random
|
|
491
|
+
*/
|
|
492
|
+
public random: RNG = function () {
|
|
493
|
+
throw new IllegalStateError('Tried to use Math.random before Workflow has been initialized');
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Used to import the user workflows
|
|
498
|
+
*
|
|
499
|
+
* Injected on isolate context startup
|
|
500
|
+
*/
|
|
501
|
+
public importWorkflows?: WorkflowsImportFunc;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Used to import the user interceptors
|
|
505
|
+
*
|
|
506
|
+
* Injected on isolate context startup
|
|
507
|
+
*/
|
|
508
|
+
public importInterceptors?: InterceptorsImportFunc;
|
|
509
|
+
|
|
510
|
+
public payloadConverter: PayloadConverter = defaultPayloadConverter;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Patches we know the status of for this workflow, as in {@link patched}
|
|
514
|
+
*/
|
|
515
|
+
public readonly knownPresentPatches = new Set<string>();
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Patches we sent to core {@link patched}
|
|
519
|
+
*/
|
|
520
|
+
public readonly sentPatches = new Set<string>();
|
|
521
|
+
|
|
522
|
+
sinkCalls = Array<SinkCall>();
|
|
523
|
+
|
|
524
|
+
getAndResetSinkCalls(): SinkCall[] {
|
|
525
|
+
const { sinkCalls } = this;
|
|
526
|
+
this.sinkCalls = [];
|
|
527
|
+
return sinkCalls;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Buffer a Workflow command to be collected at the end of the current activation.
|
|
532
|
+
*
|
|
533
|
+
* Prevents commands from being added after Workflow completion.
|
|
534
|
+
*/
|
|
535
|
+
pushCommand(cmd: coresdk.workflow_commands.IWorkflowCommand, complete = false): void {
|
|
536
|
+
// Only query responses may be sent after completion
|
|
537
|
+
if (this.completed && !cmd.respondToQuery) return;
|
|
538
|
+
this.commands.push(cmd);
|
|
539
|
+
if (complete) {
|
|
540
|
+
this.completed = true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export const state = new State();
|
|
546
|
+
|
|
547
|
+
function completeWorkflow(result: any) {
|
|
548
|
+
state.pushCommand(
|
|
549
|
+
{
|
|
550
|
+
completeWorkflowExecution: {
|
|
551
|
+
result: state.payloadConverter.toPayload(result),
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
true
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Transforms failures into a command to be sent to the server.
|
|
560
|
+
* Used to handle any failure emitted by the Workflow.
|
|
561
|
+
*/
|
|
562
|
+
export async function handleWorkflowFailure(error: unknown): Promise<void> {
|
|
563
|
+
if (state.cancelled && isCancellation(error)) {
|
|
564
|
+
state.pushCommand({ cancelWorkflowExecution: {} }, true);
|
|
565
|
+
} else if (error instanceof ContinueAsNew) {
|
|
566
|
+
state.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
|
|
567
|
+
} else {
|
|
568
|
+
if (!(error instanceof TemporalFailure)) {
|
|
569
|
+
// This results in an unhandled rejection which will fail the activation
|
|
570
|
+
// preventing it from completing.
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
state.pushCommand(
|
|
575
|
+
{
|
|
576
|
+
failWorkflowExecution: {
|
|
577
|
+
failure: errorToFailure(error, state.payloadConverter),
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
true
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function completeQuery(queryId: string, result: unknown) {
|
|
586
|
+
state.pushCommand({
|
|
587
|
+
respondToQuery: { queryId, succeeded: { response: state.payloadConverter.toPayload(result) } },
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function failQuery(queryId: string, error: any) {
|
|
592
|
+
state.pushCommand({
|
|
593
|
+
respondToQuery: { queryId, failed: errorToFailure(ensureTemporalFailure(error), state.payloadConverter) },
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Consume a completion if it exists in Workflow state */
|
|
598
|
+
export function maybeConsumeCompletion(type: keyof State['completions'], taskSeq: number): Completion | undefined {
|
|
599
|
+
const completion = state.completions[type].get(taskSeq);
|
|
600
|
+
if (completion !== undefined) {
|
|
601
|
+
state.completions[type].delete(taskSeq);
|
|
602
|
+
}
|
|
603
|
+
return completion;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Consume a completion if it exists in Workflow state, throws if it doesn't */
|
|
607
|
+
export function consumeCompletion(type: keyof State['completions'], taskSeq: number): Completion {
|
|
608
|
+
const completion = maybeConsumeCompletion(type, taskSeq);
|
|
609
|
+
if (completion === undefined) {
|
|
610
|
+
throw new IllegalStateError(`No completion for taskSeq ${taskSeq}`);
|
|
611
|
+
}
|
|
612
|
+
return completion;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function getSeq<T extends { seq?: number | null }>(activation: T): number {
|
|
616
|
+
const seq = activation.seq;
|
|
617
|
+
if (seq === undefined || seq === null) {
|
|
618
|
+
throw new TypeError(`Got activation with no seq attribute`);
|
|
619
|
+
}
|
|
620
|
+
return seq;
|
|
621
|
+
}
|
package/src/sinks.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the Workflow end of the sinks mechanism.
|
|
3
|
+
*
|
|
4
|
+
* Sinks are a mechanism for exporting data from the Workflow isolate to the
|
|
5
|
+
* Node.js environment, they are necessary because the Workflow has no way to
|
|
6
|
+
* communicate with the outside World.
|
|
7
|
+
*
|
|
8
|
+
* Sinks are typically used for exporting logs, metrics and traces out from the
|
|
9
|
+
* Workflow.
|
|
10
|
+
*
|
|
11
|
+
* Sink functions may not return values to the Workflow in order to prevent
|
|
12
|
+
* breaking determinism.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Any function signature can be used for Sink functions as long as the return type is `void`.
|
|
19
|
+
*
|
|
20
|
+
* When calling a Sink function, arguments are copied from the Workflow isolate to the Node.js environment using
|
|
21
|
+
* {@link https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist | postMessage}.
|
|
22
|
+
|
|
23
|
+
* This constrains the argument types to primitives (excluding Symbols).
|
|
24
|
+
*/
|
|
25
|
+
export type SinkFunction = (...args: any[]) => void;
|
|
26
|
+
|
|
27
|
+
/** A mapping of name to function, defines a single sink (e.g. logger) */
|
|
28
|
+
export type Sink = Record<string, SinkFunction>;
|
|
29
|
+
/**
|
|
30
|
+
* Workflow Sink are a mapping of name to {@link Sink}
|
|
31
|
+
*/
|
|
32
|
+
export type Sinks = Record<string, Sink>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Call information for a Sink
|
|
36
|
+
*/
|
|
37
|
+
export interface SinkCall {
|
|
38
|
+
ifaceName: string;
|
|
39
|
+
fnName: string;
|
|
40
|
+
args: any[];
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PromiseStackStore } from './internals';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper function to remove a promise from being tracked for stack trace query purposes
|
|
5
|
+
*/
|
|
6
|
+
export function untrackPromise(promise: Promise<unknown>): void {
|
|
7
|
+
const store = (globalThis as any).__TEMPORAL__?.promiseStackStore as PromiseStackStore | undefined;
|
|
8
|
+
if (!store) return;
|
|
9
|
+
store.childToParent.delete(promise);
|
|
10
|
+
store.promiseToStack.delete(promise);
|
|
11
|
+
}
|