@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/workflow.ts
ADDED
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
import { mapToPayloads, searchAttributePayloadConverter, toPayloads } from '@temporalio/common';
|
|
2
|
+
import {
|
|
3
|
+
ActivityFunction,
|
|
4
|
+
ActivityOptions,
|
|
5
|
+
compileRetryPolicy,
|
|
6
|
+
composeInterceptors,
|
|
7
|
+
IllegalStateError,
|
|
8
|
+
LocalActivityOptions,
|
|
9
|
+
msOptionalToTs,
|
|
10
|
+
msToNumber,
|
|
11
|
+
msToTs,
|
|
12
|
+
QueryDefinition,
|
|
13
|
+
SearchAttributes,
|
|
14
|
+
SignalDefinition,
|
|
15
|
+
tsToMs,
|
|
16
|
+
UntypedActivities,
|
|
17
|
+
WithWorkflowArgs,
|
|
18
|
+
Workflow,
|
|
19
|
+
WorkflowResultType,
|
|
20
|
+
WorkflowReturnType,
|
|
21
|
+
} from '@temporalio/internal-workflow-common';
|
|
22
|
+
import { CancellationScope, registerSleepImplementation } from './cancellation-scope';
|
|
23
|
+
import {
|
|
24
|
+
ActivityInput,
|
|
25
|
+
LocalActivityInput,
|
|
26
|
+
SignalWorkflowInput,
|
|
27
|
+
StartChildWorkflowExecutionInput,
|
|
28
|
+
TimerInput,
|
|
29
|
+
} from './interceptors';
|
|
30
|
+
import {
|
|
31
|
+
ChildWorkflowCancellationType,
|
|
32
|
+
ChildWorkflowOptions,
|
|
33
|
+
ChildWorkflowOptionsWithDefaults,
|
|
34
|
+
ContinueAsNew,
|
|
35
|
+
ContinueAsNewOptions,
|
|
36
|
+
WorkflowInfo,
|
|
37
|
+
} from './interfaces';
|
|
38
|
+
import { LocalActivityDoBackoff, state } from './internals';
|
|
39
|
+
import { Sinks } from './sinks';
|
|
40
|
+
import { untrackPromise } from './stack-helpers';
|
|
41
|
+
import { ChildWorkflowHandle, ExternalWorkflowHandle } from './workflow-handle';
|
|
42
|
+
|
|
43
|
+
// Avoid a circular dependency
|
|
44
|
+
registerSleepImplementation(sleep);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds default values to `workflowId` and `workflowIdReusePolicy` to given workflow options.
|
|
48
|
+
*/
|
|
49
|
+
export function addDefaultWorkflowOptions<T extends Workflow>(
|
|
50
|
+
opts: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
51
|
+
): ChildWorkflowOptionsWithDefaults {
|
|
52
|
+
const { args, workflowId, ...rest } = opts;
|
|
53
|
+
return {
|
|
54
|
+
workflowId: workflowId ?? uuid4(),
|
|
55
|
+
args: args ?? [],
|
|
56
|
+
cancellationType: ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
|
|
57
|
+
...rest,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Push a startTimer command into state accumulator and register completion
|
|
63
|
+
*/
|
|
64
|
+
function timerNextHandler(input: TimerInput) {
|
|
65
|
+
return new Promise<void>((resolve, reject) => {
|
|
66
|
+
const scope = CancellationScope.current();
|
|
67
|
+
if (scope.consideredCancelled) {
|
|
68
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (scope.cancellable) {
|
|
72
|
+
untrackPromise(
|
|
73
|
+
scope.cancelRequested.catch((err) => {
|
|
74
|
+
if (!state.completions.timer.delete(input.seq)) {
|
|
75
|
+
return; // Already resolved or never scheduled
|
|
76
|
+
}
|
|
77
|
+
state.pushCommand({
|
|
78
|
+
cancelTimer: {
|
|
79
|
+
seq: input.seq,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
reject(err);
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
state.pushCommand({
|
|
87
|
+
startTimer: {
|
|
88
|
+
seq: input.seq,
|
|
89
|
+
startToFireTimeout: msToTs(input.durationMs),
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
state.completions.timer.set(input.seq, {
|
|
93
|
+
resolve,
|
|
94
|
+
reject,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Asynchronous sleep.
|
|
101
|
+
*
|
|
102
|
+
* Schedules a timer on the Temporal service.
|
|
103
|
+
*
|
|
104
|
+
* @param ms sleep duration - {@link https://www.npmjs.com/package/ms | ms} formatted string or number of milliseconds.
|
|
105
|
+
* If given a negative number or 0, value will be set to 1.
|
|
106
|
+
*/
|
|
107
|
+
export function sleep(ms: number | string): Promise<void> {
|
|
108
|
+
const seq = state.nextSeqs.timer++;
|
|
109
|
+
|
|
110
|
+
const durationMs = Math.max(1, msToNumber(ms));
|
|
111
|
+
|
|
112
|
+
const execute = composeInterceptors(state.interceptors.outbound, 'startTimer', timerNextHandler);
|
|
113
|
+
|
|
114
|
+
return execute({
|
|
115
|
+
durationMs,
|
|
116
|
+
seq,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateActivityOptions(options: ActivityOptions): void {
|
|
121
|
+
if (options.scheduleToCloseTimeout === undefined && options.startToCloseTimeout === undefined) {
|
|
122
|
+
throw new TypeError('Required either scheduleToCloseTimeout or startToCloseTimeout');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Use same validation we use for normal activities
|
|
127
|
+
const validateLocalActivityOptions = validateActivityOptions;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Hooks up activity promise to current cancellation scope and completion callbacks.
|
|
131
|
+
*
|
|
132
|
+
* Returns `false` if the current scope is already cancelled.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* Push a scheduleActivity command into state accumulator and register completion
|
|
136
|
+
*/
|
|
137
|
+
function scheduleActivityNextHandler({ options, args, headers, seq, activityType }: ActivityInput): Promise<unknown> {
|
|
138
|
+
validateActivityOptions(options);
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const scope = CancellationScope.current();
|
|
141
|
+
if (scope.consideredCancelled) {
|
|
142
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (scope.cancellable) {
|
|
146
|
+
untrackPromise(
|
|
147
|
+
scope.cancelRequested.catch(() => {
|
|
148
|
+
if (!state.completions.activity.has(seq)) {
|
|
149
|
+
return; // Already resolved or never scheduled
|
|
150
|
+
}
|
|
151
|
+
state.pushCommand({
|
|
152
|
+
requestCancelActivity: {
|
|
153
|
+
seq,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
state.pushCommand({
|
|
160
|
+
scheduleActivity: {
|
|
161
|
+
seq,
|
|
162
|
+
activityId: options.activityId ?? `${seq}`,
|
|
163
|
+
activityType,
|
|
164
|
+
arguments: toPayloads(state.payloadConverter, ...args),
|
|
165
|
+
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
|
|
166
|
+
taskQueue: options.taskQueue || state.info?.taskQueue,
|
|
167
|
+
heartbeatTimeout: msOptionalToTs(options.heartbeatTimeout),
|
|
168
|
+
scheduleToCloseTimeout: msOptionalToTs(options.scheduleToCloseTimeout),
|
|
169
|
+
startToCloseTimeout: msOptionalToTs(options.startToCloseTimeout),
|
|
170
|
+
scheduleToStartTimeout: msOptionalToTs(options.scheduleToStartTimeout),
|
|
171
|
+
headers,
|
|
172
|
+
cancellationType: options.cancellationType,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
state.completions.activity.set(seq, {
|
|
176
|
+
resolve,
|
|
177
|
+
reject,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Push a scheduleActivity command into state accumulator and register completion
|
|
184
|
+
*/
|
|
185
|
+
async function scheduleLocalActivityNextHandler({
|
|
186
|
+
options,
|
|
187
|
+
args,
|
|
188
|
+
headers,
|
|
189
|
+
seq,
|
|
190
|
+
activityType,
|
|
191
|
+
attempt,
|
|
192
|
+
originalScheduleTime,
|
|
193
|
+
}: LocalActivityInput): Promise<unknown> {
|
|
194
|
+
validateLocalActivityOptions(options);
|
|
195
|
+
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const scope = CancellationScope.current();
|
|
198
|
+
if (scope.consideredCancelled) {
|
|
199
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (scope.cancellable) {
|
|
203
|
+
untrackPromise(
|
|
204
|
+
scope.cancelRequested.catch(() => {
|
|
205
|
+
if (!state.completions.activity.has(seq)) {
|
|
206
|
+
return; // Already resolved or never scheduled
|
|
207
|
+
}
|
|
208
|
+
state.pushCommand({
|
|
209
|
+
requestCancelLocalActivity: {
|
|
210
|
+
seq,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
state.pushCommand({
|
|
217
|
+
scheduleLocalActivity: {
|
|
218
|
+
seq,
|
|
219
|
+
attempt,
|
|
220
|
+
originalScheduleTime,
|
|
221
|
+
// Intentionally not exposing activityId as an option
|
|
222
|
+
activityId: `${seq}`,
|
|
223
|
+
activityType,
|
|
224
|
+
arguments: toPayloads(state.payloadConverter, ...args),
|
|
225
|
+
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
|
|
226
|
+
scheduleToCloseTimeout: msOptionalToTs(options.scheduleToCloseTimeout),
|
|
227
|
+
startToCloseTimeout: msOptionalToTs(options.startToCloseTimeout),
|
|
228
|
+
scheduleToStartTimeout: msOptionalToTs(options.scheduleToStartTimeout),
|
|
229
|
+
localRetryThreshold: msOptionalToTs(options.localRetryThreshold),
|
|
230
|
+
headers,
|
|
231
|
+
cancellationType: options.cancellationType,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
state.completions.activity.set(seq, {
|
|
235
|
+
resolve,
|
|
236
|
+
reject,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Schedule an activity and run outbound interceptors
|
|
243
|
+
* @hidden
|
|
244
|
+
*/
|
|
245
|
+
export function scheduleActivity<R>(activityType: string, args: any[], options: ActivityOptions): Promise<R> {
|
|
246
|
+
if (options === undefined) {
|
|
247
|
+
throw new TypeError('Got empty activity options');
|
|
248
|
+
}
|
|
249
|
+
const seq = state.nextSeqs.activity++;
|
|
250
|
+
const execute = composeInterceptors(state.interceptors.outbound, 'scheduleActivity', scheduleActivityNextHandler);
|
|
251
|
+
|
|
252
|
+
return execute({
|
|
253
|
+
activityType,
|
|
254
|
+
headers: {},
|
|
255
|
+
options,
|
|
256
|
+
args,
|
|
257
|
+
seq,
|
|
258
|
+
}) as Promise<R>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Schedule an activity and run outbound interceptors
|
|
263
|
+
* @hidden
|
|
264
|
+
*/
|
|
265
|
+
export async function scheduleLocalActivity<R>(
|
|
266
|
+
activityType: string,
|
|
267
|
+
args: any[],
|
|
268
|
+
options: LocalActivityOptions
|
|
269
|
+
): Promise<R> {
|
|
270
|
+
if (options === undefined) {
|
|
271
|
+
throw new TypeError('Got empty activity options');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let attempt = 1;
|
|
275
|
+
let originalScheduleTime = undefined;
|
|
276
|
+
|
|
277
|
+
for (;;) {
|
|
278
|
+
const seq = state.nextSeqs.activity++;
|
|
279
|
+
const execute = composeInterceptors(
|
|
280
|
+
state.interceptors.outbound,
|
|
281
|
+
'scheduleLocalActivity',
|
|
282
|
+
scheduleLocalActivityNextHandler
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return (await execute({
|
|
287
|
+
activityType,
|
|
288
|
+
headers: {},
|
|
289
|
+
options,
|
|
290
|
+
args,
|
|
291
|
+
seq,
|
|
292
|
+
attempt,
|
|
293
|
+
originalScheduleTime,
|
|
294
|
+
})) as Promise<R>;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (err instanceof LocalActivityDoBackoff) {
|
|
297
|
+
await sleep(tsToMs(err.backoff.backoffDuration));
|
|
298
|
+
if (typeof err.backoff.attempt !== 'number') {
|
|
299
|
+
throw new TypeError('Invalid backoff attempt type');
|
|
300
|
+
}
|
|
301
|
+
attempt = err.backoff.attempt;
|
|
302
|
+
originalScheduleTime = err.backoff.originalScheduleTime ?? undefined;
|
|
303
|
+
} else {
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function startChildWorkflowExecutionNextHandler({
|
|
311
|
+
options,
|
|
312
|
+
headers,
|
|
313
|
+
workflowType,
|
|
314
|
+
seq,
|
|
315
|
+
}: StartChildWorkflowExecutionInput): Promise<[Promise<string>, Promise<unknown>]> {
|
|
316
|
+
const workflowId = options.workflowId ?? uuid4();
|
|
317
|
+
const startPromise = new Promise<string>((resolve, reject) => {
|
|
318
|
+
const scope = CancellationScope.current();
|
|
319
|
+
if (scope.consideredCancelled) {
|
|
320
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (scope.cancellable) {
|
|
324
|
+
untrackPromise(
|
|
325
|
+
scope.cancelRequested.catch(() => {
|
|
326
|
+
const complete = !state.completions.childWorkflowComplete.has(seq);
|
|
327
|
+
const started = !state.completions.childWorkflowStart.has(seq);
|
|
328
|
+
|
|
329
|
+
if (started && !complete) {
|
|
330
|
+
const cancelSeq = state.nextSeqs.cancelWorkflow++;
|
|
331
|
+
state.pushCommand({
|
|
332
|
+
requestCancelExternalWorkflowExecution: {
|
|
333
|
+
seq: cancelSeq,
|
|
334
|
+
childWorkflowId: workflowId,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
// Not interested in this completion
|
|
338
|
+
state.completions.cancelWorkflow.set(cancelSeq, { resolve: () => undefined, reject: () => undefined });
|
|
339
|
+
} else if (!started) {
|
|
340
|
+
state.pushCommand({
|
|
341
|
+
cancelUnstartedChildWorkflowExecution: { childWorkflowSeq: seq },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Nothing to cancel otherwise
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
state.pushCommand({
|
|
349
|
+
startChildWorkflowExecution: {
|
|
350
|
+
seq,
|
|
351
|
+
workflowId,
|
|
352
|
+
workflowType,
|
|
353
|
+
input: toPayloads(state.payloadConverter, ...options.args),
|
|
354
|
+
retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
|
|
355
|
+
taskQueue: options.taskQueue || state.info?.taskQueue,
|
|
356
|
+
workflowExecutionTimeout: msOptionalToTs(options.workflowExecutionTimeout),
|
|
357
|
+
workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
|
|
358
|
+
workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
|
|
359
|
+
namespace: workflowInfo().namespace, // Not configurable
|
|
360
|
+
headers,
|
|
361
|
+
cancellationType: options.cancellationType,
|
|
362
|
+
workflowIdReusePolicy: options.workflowIdReusePolicy,
|
|
363
|
+
parentClosePolicy: options.parentClosePolicy,
|
|
364
|
+
cronSchedule: options.cronSchedule,
|
|
365
|
+
searchAttributes: options.searchAttributes
|
|
366
|
+
? mapToPayloads(searchAttributePayloadConverter, options.searchAttributes)
|
|
367
|
+
: undefined,
|
|
368
|
+
memo: options.memo && mapToPayloads(state.payloadConverter, options.memo),
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
state.completions.childWorkflowStart.set(seq, {
|
|
372
|
+
resolve,
|
|
373
|
+
reject,
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// We construct a Promise for the completion of the child Workflow before we know
|
|
378
|
+
// if the Workflow code will await it to capture the result in case it does.
|
|
379
|
+
const completePromise = new Promise((resolve, reject) => {
|
|
380
|
+
// Chain start Promise rejection to the complete Promise.
|
|
381
|
+
untrackPromise(startPromise.catch(reject));
|
|
382
|
+
state.completions.childWorkflowComplete.set(seq, {
|
|
383
|
+
resolve,
|
|
384
|
+
reject,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
untrackPromise(startPromise);
|
|
388
|
+
untrackPromise(completePromise);
|
|
389
|
+
// Prevent unhandled rejection because the completion might not be awaited
|
|
390
|
+
untrackPromise(completePromise.catch(() => undefined));
|
|
391
|
+
const ret = new Promise<[Promise<string>, Promise<unknown>]>((resolve) => resolve([startPromise, completePromise]));
|
|
392
|
+
untrackPromise(ret);
|
|
393
|
+
return ret;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function signalWorkflowNextHandler({ seq, signalName, args, target, headers }: SignalWorkflowInput) {
|
|
397
|
+
return new Promise<any>((resolve, reject) => {
|
|
398
|
+
if (state.info === undefined) {
|
|
399
|
+
throw new IllegalStateError('Workflow uninitialized');
|
|
400
|
+
}
|
|
401
|
+
const scope = CancellationScope.current();
|
|
402
|
+
if (scope.consideredCancelled) {
|
|
403
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (scope.cancellable) {
|
|
408
|
+
untrackPromise(
|
|
409
|
+
scope.cancelRequested.catch(() => {
|
|
410
|
+
if (!state.completions.signalWorkflow.has(seq)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
state.pushCommand({ cancelSignalWorkflow: { seq } });
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
state.pushCommand({
|
|
418
|
+
signalExternalWorkflowExecution: {
|
|
419
|
+
seq,
|
|
420
|
+
args: toPayloads(state.payloadConverter, ...args),
|
|
421
|
+
headers,
|
|
422
|
+
signalName,
|
|
423
|
+
...(target.type === 'external'
|
|
424
|
+
? {
|
|
425
|
+
workflowExecution: {
|
|
426
|
+
namespace: state.info.namespace,
|
|
427
|
+
...target.workflowExecution,
|
|
428
|
+
},
|
|
429
|
+
}
|
|
430
|
+
: {
|
|
431
|
+
childWorkflowId: target.childWorkflowId,
|
|
432
|
+
}),
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
state.completions.signalWorkflow.set(seq, { resolve, reject });
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Symbol used in the return type of proxy methods to mark that an attribute on the source type is not a method.
|
|
442
|
+
*
|
|
443
|
+
* @see {@link ActivityInterfaceFor}
|
|
444
|
+
* @see {@link proxyActivities}
|
|
445
|
+
* @see {@link proxyLocalActivities}
|
|
446
|
+
*/
|
|
447
|
+
export const NotAnActivityMethod = Symbol.for('__TEMPORAL_NOT_AN_ACTIVITY_METHOD');
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Type helper that takes a type `T` and transforms attributes that are not {@link ActivityFunction} to
|
|
451
|
+
* {@link NotAnActivityMethod}.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
*
|
|
455
|
+
* Used by {@link proxyActivities} to get this compile-time error:
|
|
456
|
+
*
|
|
457
|
+
* ```ts
|
|
458
|
+
* interface MyActivities {
|
|
459
|
+
* valid(input: number): Promise<number>;
|
|
460
|
+
* invalid(input: number): number;
|
|
461
|
+
* }
|
|
462
|
+
*
|
|
463
|
+
* const act = proxyActivities<MyActivities>({ startToCloseTimeout: '5m' });
|
|
464
|
+
*
|
|
465
|
+
* await act.valid(true);
|
|
466
|
+
* await act.invalid();
|
|
467
|
+
* // ^ TS complains with:
|
|
468
|
+
* // (property) invalidDefinition: typeof NotAnActivityMethod
|
|
469
|
+
* // This expression is not callable.
|
|
470
|
+
* // Type 'Symbol' has no call signatures.(2349)
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
export type ActivityInterfaceFor<T> = {
|
|
474
|
+
[K in keyof T]: T[K] extends ActivityFunction ? T[K] : typeof NotAnActivityMethod;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Configure Activity functions with given {@link ActivityOptions}.
|
|
479
|
+
*
|
|
480
|
+
* This method may be called multiple times to setup Activities with different options.
|
|
481
|
+
*
|
|
482
|
+
* @return a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy | Proxy} for
|
|
483
|
+
* which each attribute is a callable Activity function
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```ts
|
|
487
|
+
* import { proxyActivities } from '@temporalio/workflow';
|
|
488
|
+
* import * as activities from '../activities';
|
|
489
|
+
*
|
|
490
|
+
* // Setup Activities from module exports
|
|
491
|
+
* const { httpGet, otherActivity } = proxyActivities<typeof activities>({
|
|
492
|
+
* startToCloseTimeout: '30 minutes',
|
|
493
|
+
* });
|
|
494
|
+
*
|
|
495
|
+
* // Setup Activities from an explicit interface (e.g. when defined by another SDK)
|
|
496
|
+
* interface JavaActivities {
|
|
497
|
+
* httpGetFromJava(url: string): Promise<string>
|
|
498
|
+
* someOtherJavaActivity(arg1: number, arg2: string): Promise<string>;
|
|
499
|
+
* }
|
|
500
|
+
*
|
|
501
|
+
* const {
|
|
502
|
+
* httpGetFromJava,
|
|
503
|
+
* someOtherJavaActivity
|
|
504
|
+
* } = proxyActivities<JavaActivities>({
|
|
505
|
+
* taskQueue: 'java-worker-taskQueue',
|
|
506
|
+
* startToCloseTimeout: '5m',
|
|
507
|
+
* });
|
|
508
|
+
*
|
|
509
|
+
* export function execute(): Promise<void> {
|
|
510
|
+
* const response = await httpGet('http://example.com');
|
|
511
|
+
* // ...
|
|
512
|
+
* }
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
515
|
+
export function proxyActivities<A = UntypedActivities>(options: ActivityOptions): ActivityInterfaceFor<A> {
|
|
516
|
+
if (options === undefined) {
|
|
517
|
+
throw new TypeError('options must be defined');
|
|
518
|
+
}
|
|
519
|
+
// Validate as early as possible for immediate user feedback
|
|
520
|
+
validateActivityOptions(options);
|
|
521
|
+
return new Proxy(
|
|
522
|
+
{},
|
|
523
|
+
{
|
|
524
|
+
get(_, activityType) {
|
|
525
|
+
if (typeof activityType !== 'string') {
|
|
526
|
+
throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`);
|
|
527
|
+
}
|
|
528
|
+
return function activityProxyFunction(...args: unknown[]): Promise<unknown> {
|
|
529
|
+
return scheduleActivity(activityType, args, options);
|
|
530
|
+
};
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
) as any;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Configure Local Activity functions with given {@link LocalActivityOptions}.
|
|
538
|
+
*
|
|
539
|
+
* This method may be called multiple times to setup Activities with different options.
|
|
540
|
+
*
|
|
541
|
+
* @return a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy | Proxy}
|
|
542
|
+
* for which each attribute is a callable Activity function
|
|
543
|
+
*
|
|
544
|
+
* @experimental
|
|
545
|
+
*
|
|
546
|
+
* @see {@link proxyActivities} for examples
|
|
547
|
+
*/
|
|
548
|
+
export function proxyLocalActivities<A = UntypedActivities>(options: LocalActivityOptions): ActivityInterfaceFor<A> {
|
|
549
|
+
if (options === undefined) {
|
|
550
|
+
throw new TypeError('options must be defined');
|
|
551
|
+
}
|
|
552
|
+
// Validate as early as possible for immediate user feedback
|
|
553
|
+
validateLocalActivityOptions(options);
|
|
554
|
+
return new Proxy(
|
|
555
|
+
{},
|
|
556
|
+
{
|
|
557
|
+
get(_, activityType) {
|
|
558
|
+
if (typeof activityType !== 'string') {
|
|
559
|
+
throw new TypeError(`Only strings are supported for Activity types, got: ${String(activityType)}`);
|
|
560
|
+
}
|
|
561
|
+
return function localActivityProxyFunction(...args: unknown[]) {
|
|
562
|
+
return scheduleLocalActivity(activityType, args, options);
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
) as any;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// TODO: deprecate this patch after "enough" time has passed
|
|
570
|
+
const EXTERNAL_WF_CANCEL_PATCH = '__temporal_internal_connect_external_handle_cancel_to_scope';
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Returns a client-side handle that can be used to signal and cancel an existing Workflow execution.
|
|
574
|
+
* It takes a Workflow ID and optional run ID.
|
|
575
|
+
*/
|
|
576
|
+
export function getExternalWorkflowHandle(workflowId: string, runId?: string): ExternalWorkflowHandle {
|
|
577
|
+
return {
|
|
578
|
+
workflowId,
|
|
579
|
+
runId,
|
|
580
|
+
cancel() {
|
|
581
|
+
return new Promise<void>((resolve, reject) => {
|
|
582
|
+
if (state.info === undefined) {
|
|
583
|
+
throw new IllegalStateError('Uninitialized workflow');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Connect this cancel operation to the current cancellation scope.
|
|
587
|
+
// This is behavior was introduced after v0.22.0 and is incompatible
|
|
588
|
+
// with histories generated with previous SDK versions and thus requires
|
|
589
|
+
// patching.
|
|
590
|
+
//
|
|
591
|
+
// We try to delay patching as much as possible to avoid polluting
|
|
592
|
+
// histories unless strictly required.
|
|
593
|
+
const scope = CancellationScope.current();
|
|
594
|
+
if (scope.cancellable) {
|
|
595
|
+
untrackPromise(
|
|
596
|
+
scope.cancelRequested.catch((err) => {
|
|
597
|
+
if (patched(EXTERNAL_WF_CANCEL_PATCH)) {
|
|
598
|
+
reject(err);
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (scope.consideredCancelled) {
|
|
604
|
+
if (patched(EXTERNAL_WF_CANCEL_PATCH)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const seq = state.nextSeqs.cancelWorkflow++;
|
|
610
|
+
state.pushCommand({
|
|
611
|
+
requestCancelExternalWorkflowExecution: {
|
|
612
|
+
seq,
|
|
613
|
+
workflowExecution: {
|
|
614
|
+
namespace: state.info.namespace,
|
|
615
|
+
workflowId,
|
|
616
|
+
runId,
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
state.completions.cancelWorkflow.set(seq, { resolve, reject });
|
|
621
|
+
});
|
|
622
|
+
},
|
|
623
|
+
signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
|
|
624
|
+
return composeInterceptors(
|
|
625
|
+
state.interceptors.outbound,
|
|
626
|
+
'signalWorkflow',
|
|
627
|
+
signalWorkflowNextHandler
|
|
628
|
+
)({
|
|
629
|
+
seq: state.nextSeqs.signalWorkflow++,
|
|
630
|
+
signalName: typeof def === 'string' ? def : def.name,
|
|
631
|
+
args,
|
|
632
|
+
target: {
|
|
633
|
+
type: 'external',
|
|
634
|
+
workflowExecution: { workflowId, runId },
|
|
635
|
+
},
|
|
636
|
+
headers: {},
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Start a child Workflow execution
|
|
644
|
+
*
|
|
645
|
+
* - Returns a client-side handle that implements a child Workflow interface.
|
|
646
|
+
* - By default, a child will be scheduled on the same task queue as its parent.
|
|
647
|
+
*
|
|
648
|
+
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
|
|
649
|
+
* In order to query the child, use a {@link WorkflowClient} from an Activity.
|
|
650
|
+
*/
|
|
651
|
+
export async function startChild<T extends Workflow>(
|
|
652
|
+
workflowType: string,
|
|
653
|
+
options: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
654
|
+
): Promise<ChildWorkflowHandle<T>>;
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Start a child Workflow execution
|
|
658
|
+
*
|
|
659
|
+
* - Returns a client-side handle that implements a child Workflow interface.
|
|
660
|
+
* - Deduces the Workflow type and signature from provided Workflow function.
|
|
661
|
+
* - By default, a child will be scheduled on the same task queue as its parent.
|
|
662
|
+
*
|
|
663
|
+
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
|
|
664
|
+
* In order to query the child, use a {@link WorkflowClient} from an Activity.
|
|
665
|
+
*/
|
|
666
|
+
export async function startChild<T extends Workflow>(
|
|
667
|
+
workflowFunc: T,
|
|
668
|
+
options: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
669
|
+
): Promise<ChildWorkflowHandle<T>>;
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Start a child Workflow execution
|
|
673
|
+
*
|
|
674
|
+
* **Override for Workflows that accept no arguments**.
|
|
675
|
+
*
|
|
676
|
+
* - Returns a client-side handle that implements a child Workflow interface.
|
|
677
|
+
* - The child will be scheduled on the same task queue as its parent.
|
|
678
|
+
*
|
|
679
|
+
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
|
|
680
|
+
* In order to query the child, use a {@link WorkflowClient} from an Activity.
|
|
681
|
+
*/
|
|
682
|
+
export async function startChild<T extends () => Promise<any>>(workflowType: string): Promise<ChildWorkflowHandle<T>>;
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Start a child Workflow execution
|
|
686
|
+
*
|
|
687
|
+
* **Override for Workflows that accept no arguments**.
|
|
688
|
+
*
|
|
689
|
+
* - Returns a client-side handle that implements a child Workflow interface.
|
|
690
|
+
* - Deduces the Workflow type and signature from provided Workflow function.
|
|
691
|
+
* - The child will be scheduled on the same task queue as its parent.
|
|
692
|
+
*
|
|
693
|
+
* A child Workflow handle supports awaiting completion, signaling and cancellation via {@link CancellationScope}s.
|
|
694
|
+
* In order to query the child, use a {@link WorkflowClient} from an Activity.
|
|
695
|
+
*/
|
|
696
|
+
export async function startChild<T extends () => Promise<any>>(workflowFunc: T): Promise<ChildWorkflowHandle<T>>;
|
|
697
|
+
|
|
698
|
+
export async function startChild<T extends Workflow>(
|
|
699
|
+
workflowTypeOrFunc: string | T,
|
|
700
|
+
options?: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
701
|
+
): Promise<ChildWorkflowHandle<T>> {
|
|
702
|
+
const optionsWithDefaults = addDefaultWorkflowOptions(options ?? {});
|
|
703
|
+
const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
|
|
704
|
+
const execute = composeInterceptors(
|
|
705
|
+
state.interceptors.outbound,
|
|
706
|
+
'startChildWorkflowExecution',
|
|
707
|
+
startChildWorkflowExecutionNextHandler
|
|
708
|
+
);
|
|
709
|
+
const [started, completed] = await execute({
|
|
710
|
+
seq: state.nextSeqs.childWorkflow++,
|
|
711
|
+
options: optionsWithDefaults,
|
|
712
|
+
headers: {},
|
|
713
|
+
workflowType,
|
|
714
|
+
});
|
|
715
|
+
const firstExecutionRunId = await started;
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
workflowId: optionsWithDefaults.workflowId,
|
|
719
|
+
firstExecutionRunId,
|
|
720
|
+
async result(): Promise<WorkflowResultType<T>> {
|
|
721
|
+
return (await completed) as any;
|
|
722
|
+
},
|
|
723
|
+
async signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
|
|
724
|
+
return composeInterceptors(
|
|
725
|
+
state.interceptors.outbound,
|
|
726
|
+
'signalWorkflow',
|
|
727
|
+
signalWorkflowNextHandler
|
|
728
|
+
)({
|
|
729
|
+
seq: state.nextSeqs.signalWorkflow++,
|
|
730
|
+
signalName: typeof def === 'string' ? def : def.name,
|
|
731
|
+
args,
|
|
732
|
+
target: {
|
|
733
|
+
type: 'child',
|
|
734
|
+
childWorkflowId: optionsWithDefaults.workflowId,
|
|
735
|
+
},
|
|
736
|
+
headers: {},
|
|
737
|
+
});
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Start a child Workflow execution and await its completion.
|
|
744
|
+
*
|
|
745
|
+
* - By default, a child will be scheduled on the same task queue as its parent.
|
|
746
|
+
* - This operation is cancellable using {@link CancellationScope}s.
|
|
747
|
+
*
|
|
748
|
+
* @return The result of the child Workflow.
|
|
749
|
+
*/
|
|
750
|
+
export async function executeChild<T extends Workflow>(
|
|
751
|
+
workflowType: string,
|
|
752
|
+
options: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
753
|
+
): Promise<WorkflowResultType<T>>;
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Start a child Workflow execution and await its completion.
|
|
757
|
+
*
|
|
758
|
+
* - By default, a child will be scheduled on the same task queue as its parent.
|
|
759
|
+
* - Deduces the Workflow type and signature from provided Workflow function.
|
|
760
|
+
* - This operation is cancellable using {@link CancellationScope}s.
|
|
761
|
+
*
|
|
762
|
+
* @return The result of the child Workflow.
|
|
763
|
+
*/
|
|
764
|
+
export async function executeChild<T extends Workflow>(
|
|
765
|
+
workflowType: T,
|
|
766
|
+
options: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
767
|
+
): Promise<WorkflowResultType<T>>;
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Start a child Workflow execution and await its completion.
|
|
771
|
+
*
|
|
772
|
+
* **Override for Workflows that accept no arguments**.
|
|
773
|
+
*
|
|
774
|
+
* - The child will be scheduled on the same task queue as its parent.
|
|
775
|
+
* - This operation is cancellable using {@link CancellationScope}s.
|
|
776
|
+
*
|
|
777
|
+
* @return The result of the child Workflow.
|
|
778
|
+
*/
|
|
779
|
+
export async function executeChild<T extends () => WorkflowReturnType>(
|
|
780
|
+
workflowType: string
|
|
781
|
+
): Promise<WorkflowResultType<T>>;
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Start a child Workflow execution and await its completion.
|
|
785
|
+
*
|
|
786
|
+
* **Override for Workflows that accept no arguments**.
|
|
787
|
+
*
|
|
788
|
+
* - The child will be scheduled on the same task queue as its parent.
|
|
789
|
+
* - Deduces the Workflow type and signature from provided Workflow function.
|
|
790
|
+
* - This operation is cancellable using {@link CancellationScope}s.
|
|
791
|
+
*
|
|
792
|
+
* @return The result of the child Workflow.
|
|
793
|
+
*/
|
|
794
|
+
export async function executeChild<T extends () => WorkflowReturnType>(workflowFunc: T): Promise<WorkflowResultType<T>>;
|
|
795
|
+
|
|
796
|
+
export async function executeChild<T extends Workflow>(
|
|
797
|
+
workflowTypeOrFunc: string | T,
|
|
798
|
+
options?: WithWorkflowArgs<T, ChildWorkflowOptions>
|
|
799
|
+
): Promise<WorkflowResultType<T>> {
|
|
800
|
+
const optionsWithDefaults = addDefaultWorkflowOptions(options ?? {});
|
|
801
|
+
const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
|
|
802
|
+
const execute = composeInterceptors(
|
|
803
|
+
state.interceptors.outbound,
|
|
804
|
+
'startChildWorkflowExecution',
|
|
805
|
+
startChildWorkflowExecutionNextHandler
|
|
806
|
+
);
|
|
807
|
+
const execPromise = execute({
|
|
808
|
+
seq: state.nextSeqs.childWorkflow++,
|
|
809
|
+
options: optionsWithDefaults,
|
|
810
|
+
headers: {},
|
|
811
|
+
workflowType,
|
|
812
|
+
});
|
|
813
|
+
untrackPromise(execPromise);
|
|
814
|
+
const completedPromise = execPromise.then(([_started, completed]) => completed);
|
|
815
|
+
untrackPromise(completedPromise);
|
|
816
|
+
return completedPromise as Promise<any>;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Get information about the current Workflow
|
|
821
|
+
*/
|
|
822
|
+
export function workflowInfo(): WorkflowInfo {
|
|
823
|
+
if (state.info === undefined) {
|
|
824
|
+
throw new IllegalStateError('Workflow uninitialized');
|
|
825
|
+
}
|
|
826
|
+
return state.info;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Returns whether or not code is executing in workflow context
|
|
831
|
+
*/
|
|
832
|
+
export function inWorkflowContext(): boolean {
|
|
833
|
+
try {
|
|
834
|
+
workflowInfo();
|
|
835
|
+
return true;
|
|
836
|
+
} catch (err: any) {
|
|
837
|
+
// Use string comparison in case multiple versions of @temporalio/common are
|
|
838
|
+
// installed in which case an instanceof check would fail.
|
|
839
|
+
if (err.name === 'IllegalStateError') {
|
|
840
|
+
return false;
|
|
841
|
+
} else {
|
|
842
|
+
throw err;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Get a reference to Sinks for exporting data out of the Workflow.
|
|
849
|
+
*
|
|
850
|
+
* These Sinks **must** be registered with the Worker in order for this
|
|
851
|
+
* mechanism to work.
|
|
852
|
+
*
|
|
853
|
+
* @example
|
|
854
|
+
* ```ts
|
|
855
|
+
* import { proxySinks, Sinks } from '@temporalio/workflow';
|
|
856
|
+
*
|
|
857
|
+
* interface MySinks extends Sinks {
|
|
858
|
+
* logger: {
|
|
859
|
+
* info(message: string): void;
|
|
860
|
+
* error(message: string): void;
|
|
861
|
+
* };
|
|
862
|
+
* }
|
|
863
|
+
*
|
|
864
|
+
* const { logger } = proxySinks<MyDependencies>();
|
|
865
|
+
* logger.info('setting up');
|
|
866
|
+
*
|
|
867
|
+
* export function myWorkflow() {
|
|
868
|
+
* return {
|
|
869
|
+
* async execute() {
|
|
870
|
+
* logger.info('hey ho');
|
|
871
|
+
* logger.error('lets go');
|
|
872
|
+
* }
|
|
873
|
+
* };
|
|
874
|
+
* }
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
export function proxySinks<T extends Sinks>(): T {
|
|
878
|
+
return new Proxy(
|
|
879
|
+
{},
|
|
880
|
+
{
|
|
881
|
+
get(_, ifaceName) {
|
|
882
|
+
return new Proxy(
|
|
883
|
+
{},
|
|
884
|
+
{
|
|
885
|
+
get(_, fnName) {
|
|
886
|
+
return (...args: any[]) => {
|
|
887
|
+
state.sinkCalls.push({
|
|
888
|
+
ifaceName: ifaceName as string,
|
|
889
|
+
fnName: fnName as string,
|
|
890
|
+
args,
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
},
|
|
894
|
+
}
|
|
895
|
+
);
|
|
896
|
+
},
|
|
897
|
+
}
|
|
898
|
+
) as any;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Returns a function `f` that will cause the current Workflow to ContinueAsNew when called.
|
|
903
|
+
*
|
|
904
|
+
* `f` takes the same arguments as the Workflow function supplied to typeparam `F`.
|
|
905
|
+
*
|
|
906
|
+
* Once `f` is called, Workflow Execution immediately completes.
|
|
907
|
+
*/
|
|
908
|
+
export function makeContinueAsNewFunc<F extends Workflow>(
|
|
909
|
+
options?: ContinueAsNewOptions
|
|
910
|
+
): (...args: Parameters<F>) => Promise<never> {
|
|
911
|
+
const info = workflowInfo();
|
|
912
|
+
const { workflowType, taskQueue, ...rest } = options ?? {};
|
|
913
|
+
const requiredOptions = {
|
|
914
|
+
workflowType: workflowType ?? info.workflowType,
|
|
915
|
+
taskQueue: taskQueue ?? info.taskQueue,
|
|
916
|
+
...rest,
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
return (...args: Parameters<F>): Promise<never> => {
|
|
920
|
+
const fn = composeInterceptors(state.interceptors.outbound, 'continueAsNew', async (input) => {
|
|
921
|
+
const { headers, args, options } = input;
|
|
922
|
+
throw new ContinueAsNew({
|
|
923
|
+
workflowType: options.workflowType,
|
|
924
|
+
arguments: toPayloads(state.payloadConverter, ...args),
|
|
925
|
+
headers,
|
|
926
|
+
taskQueue: options.taskQueue,
|
|
927
|
+
memo: options.memo,
|
|
928
|
+
searchAttributes: options.searchAttributes
|
|
929
|
+
? mapToPayloads(searchAttributePayloadConverter, options.searchAttributes)
|
|
930
|
+
: undefined,
|
|
931
|
+
workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
|
|
932
|
+
workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
return fn({
|
|
936
|
+
args,
|
|
937
|
+
headers: {},
|
|
938
|
+
options: requiredOptions,
|
|
939
|
+
});
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* {@link https://docs.temporal.io/concepts/what-is-continue-as-new/ | Continues-As-New} the current Workflow Execution
|
|
945
|
+
* with default options.
|
|
946
|
+
*
|
|
947
|
+
* Shorthand for `makeContinueAsNewFunc<F>()(...args)`. (See: {@link makeContinueAsNewFunc}.)
|
|
948
|
+
*
|
|
949
|
+
* @example
|
|
950
|
+
*
|
|
951
|
+
*```ts
|
|
952
|
+
*import { continueAsNew } from '@temporalio/workflow';
|
|
953
|
+
*
|
|
954
|
+
*export async function myWorkflow(n: number): Promise<void> {
|
|
955
|
+
* // ... Workflow logic
|
|
956
|
+
* await continueAsNew<typeof myWorkflow>(n + 1);
|
|
957
|
+
*}
|
|
958
|
+
*```
|
|
959
|
+
*/
|
|
960
|
+
export function continueAsNew<F extends Workflow>(...args: Parameters<F>): Promise<never> {
|
|
961
|
+
return makeContinueAsNewFunc()(...args);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Generate an RFC compliant V4 uuid.
|
|
966
|
+
* Uses the workflow's deterministic PRNG making it safe for use within a workflow.
|
|
967
|
+
* This function is cryptographically insecure.
|
|
968
|
+
* See the {@link https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid | stackoverflow discussion}.
|
|
969
|
+
*/
|
|
970
|
+
export function uuid4(): string {
|
|
971
|
+
// Return the hexadecimal text representation of number `n`, padded with zeroes to be of length `p`
|
|
972
|
+
const ho = (n: number, p: number) => n.toString(16).padStart(p, '0');
|
|
973
|
+
// Create a view backed by a 16-byte buffer
|
|
974
|
+
const view = new DataView(new ArrayBuffer(16));
|
|
975
|
+
// Fill buffer with random values
|
|
976
|
+
view.setUint32(0, (Math.random() * 0x100000000) >>> 0);
|
|
977
|
+
view.setUint32(4, (Math.random() * 0x100000000) >>> 0);
|
|
978
|
+
view.setUint32(8, (Math.random() * 0x100000000) >>> 0);
|
|
979
|
+
view.setUint32(12, (Math.random() * 0x100000000) >>> 0);
|
|
980
|
+
// Patch the 6th byte to reflect a version 4 UUID
|
|
981
|
+
view.setUint8(6, (view.getUint8(6) & 0xf) | 0x40);
|
|
982
|
+
// Patch the 8th byte to reflect a variant 1 UUID (version 4 UUIDs are)
|
|
983
|
+
view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80);
|
|
984
|
+
// Compile the canonical textual form from the array data
|
|
985
|
+
return `${ho(view.getUint32(0), 8)}-${ho(view.getUint16(4), 4)}-${ho(view.getUint16(6), 4)}-${ho(
|
|
986
|
+
view.getUint16(8),
|
|
987
|
+
4
|
|
988
|
+
)}-${ho(view.getUint32(10), 8)}${ho(view.getUint16(14), 4)}`;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Patch or upgrade workflow code by checking or stating that this workflow has a certain patch.
|
|
993
|
+
*
|
|
994
|
+
* See {@link https://docs.temporal.io/typescript/versioning | docs page} for info.
|
|
995
|
+
*
|
|
996
|
+
* If the workflow is replaying an existing history, then this function returns true if that
|
|
997
|
+
* history was produced by a worker which also had a `patched` call with the same `patchId`.
|
|
998
|
+
* If the history was produced by a worker *without* such a call, then it will return false.
|
|
999
|
+
*
|
|
1000
|
+
* If the workflow is not currently replaying, then this call *always* returns true.
|
|
1001
|
+
*
|
|
1002
|
+
* Your workflow code should run the "new" code if this returns true, if it returns false, you
|
|
1003
|
+
* should run the "old" code. By doing this, you can maintain determinism.
|
|
1004
|
+
*
|
|
1005
|
+
* @param patchId An identifier that should be unique to this patch. It is OK to use multiple
|
|
1006
|
+
* calls with the same ID, which means all such calls will always return the same value.
|
|
1007
|
+
*/
|
|
1008
|
+
export function patched(patchId: string): boolean {
|
|
1009
|
+
return patchInternal(patchId, false);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Indicate that a patch is being phased out.
|
|
1014
|
+
*
|
|
1015
|
+
* See {@link https://docs.temporal.io/typescript/versioning | docs page} for info.
|
|
1016
|
+
*
|
|
1017
|
+
* Workflows with this call may be deployed alongside workflows with a {@link patched} call, but
|
|
1018
|
+
* they must *not* be deployed while any workers still exist running old code without a
|
|
1019
|
+
* {@link patched} call, or any runs with histories produced by such workers exist. If either kind
|
|
1020
|
+
* of worker encounters a history produced by the other, their behavior is undefined.
|
|
1021
|
+
*
|
|
1022
|
+
* Once all live workflow runs have been produced by workers with this call, you can deploy workers
|
|
1023
|
+
* which are free of either kind of patch call for this ID. Workers with and without this call
|
|
1024
|
+
* may coexist, as long as they are both running the "new" code.
|
|
1025
|
+
*
|
|
1026
|
+
* @param patchId An identifier that should be unique to this patch. It is OK to use multiple
|
|
1027
|
+
* calls with the same ID, which means all such calls will always return the same value.
|
|
1028
|
+
*/
|
|
1029
|
+
export function deprecatePatch(patchId: string): void {
|
|
1030
|
+
patchInternal(patchId, true);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function patchInternal(patchId: string, deprecated: boolean): boolean {
|
|
1034
|
+
// Patch operation does not support interception at the moment, if it did,
|
|
1035
|
+
// this would be the place to start the interception chain
|
|
1036
|
+
|
|
1037
|
+
if (state.workflow === undefined) {
|
|
1038
|
+
throw new IllegalStateError('Patches cannot be used before Workflow starts');
|
|
1039
|
+
}
|
|
1040
|
+
const usePatch = !state.isReplaying || state.knownPresentPatches.has(patchId);
|
|
1041
|
+
// Avoid sending commands for patches core already knows about.
|
|
1042
|
+
// This optimization enables development of automatic patching tools.
|
|
1043
|
+
if (usePatch && !state.sentPatches.has(patchId)) {
|
|
1044
|
+
state.pushCommand({
|
|
1045
|
+
setPatchMarker: { patchId, deprecated },
|
|
1046
|
+
});
|
|
1047
|
+
state.sentPatches.add(patchId);
|
|
1048
|
+
}
|
|
1049
|
+
return usePatch;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Returns a Promise that resolves when `fn` evaluates to `true` or `timeout` expires.
|
|
1054
|
+
*
|
|
1055
|
+
* @param timeout {@link https://www.npmjs.com/package/ms | ms} formatted string or number of milliseconds
|
|
1056
|
+
*
|
|
1057
|
+
* @returns a boolean indicating whether the condition was true before the timeout expires
|
|
1058
|
+
*/
|
|
1059
|
+
export function condition(fn: () => boolean, timeout: number | string): Promise<boolean>;
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Returns a Promise that resolves when `fn` evaluates to `true`.
|
|
1063
|
+
*/
|
|
1064
|
+
export function condition(fn: () => boolean): Promise<void>;
|
|
1065
|
+
|
|
1066
|
+
export async function condition(fn: () => boolean, timeout?: number | string): Promise<void | boolean> {
|
|
1067
|
+
if (timeout) {
|
|
1068
|
+
return CancellationScope.cancellable(async () => {
|
|
1069
|
+
try {
|
|
1070
|
+
return await Promise.race([sleep(timeout).then(() => false), conditionInner(fn).then(() => true)]);
|
|
1071
|
+
} finally {
|
|
1072
|
+
CancellationScope.current().cancel();
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return conditionInner(fn);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function conditionInner(fn: () => boolean): Promise<void> {
|
|
1080
|
+
return new Promise((resolve, reject) => {
|
|
1081
|
+
const scope = CancellationScope.current();
|
|
1082
|
+
if (scope.consideredCancelled) {
|
|
1083
|
+
untrackPromise(scope.cancelRequested.catch(reject));
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const seq = state.nextSeqs.condition++;
|
|
1088
|
+
if (scope.cancellable) {
|
|
1089
|
+
untrackPromise(
|
|
1090
|
+
scope.cancelRequested.catch((err) => {
|
|
1091
|
+
state.blockedConditions.delete(seq);
|
|
1092
|
+
reject(err);
|
|
1093
|
+
})
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Eager evaluation
|
|
1098
|
+
if (fn()) {
|
|
1099
|
+
resolve();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
state.blockedConditions.set(seq, { fn, resolve });
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Define a signal method for a Workflow.
|
|
1109
|
+
*
|
|
1110
|
+
* Definitions are used to register handler in the Workflow via {@link setHandler} and to signal Workflows using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
|
|
1111
|
+
* Definitions can be reused in multiple Workflows.
|
|
1112
|
+
*/
|
|
1113
|
+
export function defineSignal<Args extends any[] = []>(name: string): SignalDefinition<Args> {
|
|
1114
|
+
return {
|
|
1115
|
+
type: 'signal',
|
|
1116
|
+
name,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Define a query method for a Workflow.
|
|
1122
|
+
*
|
|
1123
|
+
* Definitions are used to register handler in the Workflow via {@link setHandler} and to query Workflows using a {@link WorkflowHandle}.
|
|
1124
|
+
* Definitions can be reused in multiple Workflows.
|
|
1125
|
+
*/
|
|
1126
|
+
export function defineQuery<Ret, Args extends any[] = []>(name: string): QueryDefinition<Ret, Args> {
|
|
1127
|
+
return {
|
|
1128
|
+
type: 'query',
|
|
1129
|
+
name,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* A handler function capable of accepting the arguments for a given SignalDefinition or QueryDefinition.
|
|
1135
|
+
*/
|
|
1136
|
+
export type Handler<
|
|
1137
|
+
Ret,
|
|
1138
|
+
Args extends any[],
|
|
1139
|
+
T extends SignalDefinition<Args> | QueryDefinition<Ret, Args>
|
|
1140
|
+
> = T extends SignalDefinition<infer A>
|
|
1141
|
+
? (...args: A) => void | Promise<void>
|
|
1142
|
+
: T extends QueryDefinition<infer R, infer A>
|
|
1143
|
+
? (...args: A) => R
|
|
1144
|
+
: never;
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Set a handler function for a Workflow query or signal.
|
|
1148
|
+
*
|
|
1149
|
+
* If this function is called multiple times for a given signal or query name the last handler will overwrite any previous calls.
|
|
1150
|
+
*
|
|
1151
|
+
* @param def a {@link SignalDefinition} or {@link QueryDefinition} as returned by {@link defineSignal} or {@link defineQuery} respectively.
|
|
1152
|
+
* @param handler a compatible handler function for the given definition or `undefined` to unset the handler.
|
|
1153
|
+
*/
|
|
1154
|
+
export function setHandler<Ret, Args extends any[], T extends SignalDefinition<Args> | QueryDefinition<Ret, Args>>(
|
|
1155
|
+
def: T,
|
|
1156
|
+
handler: Handler<Ret, Args, T> | undefined
|
|
1157
|
+
): void {
|
|
1158
|
+
if (def.type === 'signal') {
|
|
1159
|
+
state.signalHandlers.set(def.name, handler as any);
|
|
1160
|
+
const bufferedSignals = state.bufferedSignals.get(def.name);
|
|
1161
|
+
if (bufferedSignals !== undefined && handler !== undefined) {
|
|
1162
|
+
state.bufferedSignals.delete(def.name);
|
|
1163
|
+
for (const signal of bufferedSignals) {
|
|
1164
|
+
state.activator.signalWorkflow(signal);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
} else if (def.type === 'query') {
|
|
1168
|
+
state.queryHandlers.set(def.name, handler as any);
|
|
1169
|
+
} else {
|
|
1170
|
+
throw new TypeError(`Invalid definition type: ${(def as any).type}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Updates this Workflow's Search Attributes by merging the provided `searchAttributes` with the existing Search
|
|
1176
|
+
* Attributes, `workflowInfo().searchAttributes`.
|
|
1177
|
+
*
|
|
1178
|
+
* For example, this Workflow code:
|
|
1179
|
+
*
|
|
1180
|
+
* ```ts
|
|
1181
|
+
* upsertSearchAttributes({
|
|
1182
|
+
* CustomIntField: [1, 2, 3],
|
|
1183
|
+
* CustomBoolField: [true]
|
|
1184
|
+
* });
|
|
1185
|
+
* upsertSearchAttributes({
|
|
1186
|
+
* CustomIntField: [42],
|
|
1187
|
+
* CustomKeywordField: ['durable code', 'is great']
|
|
1188
|
+
* });
|
|
1189
|
+
* ```
|
|
1190
|
+
*
|
|
1191
|
+
* would result in the Workflow having these Search Attributes:
|
|
1192
|
+
*
|
|
1193
|
+
* ```ts
|
|
1194
|
+
* {
|
|
1195
|
+
* CustomIntField: [42],
|
|
1196
|
+
* CustomBoolField: [true],
|
|
1197
|
+
* CustomKeywordField: ['durable code', 'is great']
|
|
1198
|
+
* }
|
|
1199
|
+
* ```
|
|
1200
|
+
*
|
|
1201
|
+
* @param searchAttributes The Record to merge. Use a value of `[]` to clear a Search Attribute.
|
|
1202
|
+
*/
|
|
1203
|
+
export function upsertSearchAttributes(searchAttributes: SearchAttributes): void {
|
|
1204
|
+
if (!state.info) {
|
|
1205
|
+
throw new IllegalStateError('`state.info` should be defined');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const mergedSearchAttributes = { ...state.info.searchAttributes, ...searchAttributes };
|
|
1209
|
+
if (!mergedSearchAttributes) {
|
|
1210
|
+
throw new Error('searchAttributes must be a non-null SearchAttributes');
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
state.pushCommand({
|
|
1214
|
+
upsertWorkflowSearchAttributes: {
|
|
1215
|
+
searchAttributes: mapToPayloads(searchAttributePayloadConverter, searchAttributes),
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
state.info.searchAttributes = mergedSearchAttributes;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Unsafe information about the currently executing Workflow Task.
|
|
1224
|
+
*
|
|
1225
|
+
* Never rely on this information in Workflow logic as it will cause non-deterministic behavior.
|
|
1226
|
+
*/
|
|
1227
|
+
export interface UnsafeTaskInfo {
|
|
1228
|
+
isReplaying: boolean;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Information about the currently executing Workflow Task.
|
|
1233
|
+
*
|
|
1234
|
+
* Meant for advanced usage.
|
|
1235
|
+
*/
|
|
1236
|
+
export interface TaskInfo {
|
|
1237
|
+
/**
|
|
1238
|
+
* Length of Workflow history up until the current Workflow Task.
|
|
1239
|
+
*
|
|
1240
|
+
* You may safely use this information to decide when to {@link continueAsNew}.
|
|
1241
|
+
*/
|
|
1242
|
+
historyLength: number;
|
|
1243
|
+
unsafe: UnsafeTaskInfo;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Get information about the currently executing Workflow Task.
|
|
1248
|
+
*
|
|
1249
|
+
* See {@link TaskInfo}
|
|
1250
|
+
*/
|
|
1251
|
+
export function taskInfo(): TaskInfo {
|
|
1252
|
+
const { isReplaying, historyLength } = state;
|
|
1253
|
+
if (isReplaying == null || historyLength == null) {
|
|
1254
|
+
throw new IllegalStateError('Workflow uninitialized');
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
historyLength,
|
|
1259
|
+
unsafe: {
|
|
1260
|
+
isReplaying,
|
|
1261
|
+
},
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
export const stackTraceQuery = defineQuery<string>('__stack_trace');
|