@temporalio/workflow 1.4.4 → 1.5.0

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/src/internals.ts CHANGED
@@ -1,29 +1,35 @@
1
- import { defaultFailureConverter, FailureConverter, PayloadConverter } from '@temporalio/common';
2
1
  import type { RawSourceMap } from 'source-map';
3
2
  import {
3
+ defaultFailureConverter,
4
+ FailureConverter,
5
+ PayloadConverter,
4
6
  arrayFromPayloads,
5
7
  defaultPayloadConverter,
6
8
  ensureTemporalFailure,
7
9
  IllegalStateError,
8
10
  TemporalFailure,
9
11
  Workflow,
12
+ WorkflowExecutionAlreadyStartedError,
10
13
  WorkflowQueryType,
11
14
  WorkflowSignalType,
15
+ ProtoFailure,
12
16
  } from '@temporalio/common';
13
17
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
14
18
  import { checkExtends } from '@temporalio/common/lib/type-helpers';
15
19
  import type { coresdk } from '@temporalio/proto';
16
20
  import { alea, RNG } from './alea';
17
- import { ROOT_SCOPE } from './cancellation-scope';
18
- import { DeterminismViolationError, isCancellation, WorkflowExecutionAlreadyStartedError } from './errors';
21
+ import { RootCancellationScope } from './cancellation-scope';
22
+ import { DeterminismViolationError, isCancellation } from './errors';
23
+ import { QueryInput, SignalInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
19
24
  import {
20
- QueryInput,
21
- SignalInput,
22
- WorkflowExecuteInput,
23
- WorkflowInterceptors,
24
- WorkflowInterceptorsFactory,
25
- } from './interceptors';
26
- import { ContinueAsNew, SDKInfo, FileSlice, EnhancedStackTrace, FileLocation, WorkflowInfo } from './interfaces';
25
+ ContinueAsNew,
26
+ SDKInfo,
27
+ FileSlice,
28
+ EnhancedStackTrace,
29
+ FileLocation,
30
+ WorkflowInfo,
31
+ WorkflowCreateOptionsWithSourceMap,
32
+ } from './interfaces';
27
33
  import { SinkCall } from './sinks';
28
34
  import { untrackPromise } from './stack-helpers';
29
35
  import pkg from './pkg';
@@ -49,12 +55,9 @@ export interface PromiseStackStore {
49
55
  promiseToStack: Map<Promise<unknown>, Stack>;
50
56
  }
51
57
 
52
- export type ResolveFunction<T = any> = (val: T) => any;
53
- export type RejectFunction<E = any> = (val: E) => any;
54
-
55
58
  export interface Completion {
56
- resolve: ResolveFunction;
57
- reject: RejectFunction;
59
+ resolve(val: unknown): unknown;
60
+ reject(reason: unknown): unknown;
58
61
  }
59
62
 
60
63
  export interface Condition {
@@ -74,15 +77,278 @@ export type ActivationHandlerFunction<K extends keyof coresdk.workflow_activatio
74
77
  activation: NonNullable<coresdk.workflow_activation.IWorkflowActivationJob[K]>
75
78
  ) => void;
76
79
 
80
+ /**
81
+ * Verifies all activation job handling methods are implemented
82
+ */
77
83
  export type ActivationHandler = {
78
84
  [P in keyof coresdk.workflow_activation.IWorkflowActivationJob]: ActivationHandlerFunction<P>;
79
85
  };
80
86
 
87
+ /**
88
+ * SDK Internal Patches are created by the SDK to avoid breaking history when behaviour
89
+ * of existing API need to be modified. This is the patch number supported by the current
90
+ * version of the SDK.
91
+ *
92
+ * History:
93
+ * 1: Fix `condition(..., 0)` is not the same as `condition(..., undefined)`
94
+ */
95
+ export const LATEST_INTERNAL_PATCH_NUMBER = 1;
96
+
97
+ /**
98
+ * Keeps all of the Workflow runtime state like pending completions for activities and timers.
99
+ *
100
+ * Implements handlers for all workflow activation jobs.
101
+ */
81
102
  export class Activator implements ActivationHandler {
82
- workflowFunctionWasCalled = false;
103
+ /**
104
+ * Map of task sequence to a Completion
105
+ */
106
+ readonly completions = {
107
+ timer: new Map<number, Completion>(),
108
+ activity: new Map<number, Completion>(),
109
+ childWorkflowStart: new Map<number, Completion>(),
110
+ childWorkflowComplete: new Map<number, Completion>(),
111
+ signalWorkflow: new Map<number, Completion>(),
112
+ cancelWorkflow: new Map<number, Completion>(),
113
+ };
114
+
115
+ /**
116
+ * Holds buffered signal calls until a handler is registered
117
+ */
118
+ readonly bufferedSignals = new Map<string, coresdk.workflow_activation.ISignalWorkflow[]>();
119
+
120
+ /**
121
+ * Holds buffered query calls until a handler is registered.
122
+ *
123
+ * **IMPORTANT** queries are only buffered until workflow is started.
124
+ * This is required because async interceptors might block workflow function invocation
125
+ * which delays query handler registration.
126
+ */
127
+ protected readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
128
+
129
+ /**
130
+ * Mapping of signal name to handler
131
+ */
132
+ readonly signalHandlers = new Map<string, WorkflowSignalType>();
133
+
134
+ /**
135
+ * Source map file for looking up the source files in response to __enhanced_stack_trace
136
+ */
137
+ protected readonly sourceMap: RawSourceMap;
138
+
139
+ /**
140
+ * Whether or not to send the sources in enhanced stack trace query responses
141
+ */
142
+ protected readonly showStackTraceSources;
143
+
144
+ protected readonly promiseStackStore: PromiseStackStore = {
145
+ promiseToStack: new Map(),
146
+ childToParent: new Map(),
147
+ };
148
+
149
+ public readonly rootScope = new RootCancellationScope();
150
+
151
+ /**
152
+ * Mapping of query name to handler
153
+ */
154
+ public readonly queryHandlers = new Map<string, WorkflowQueryType>([
155
+ [
156
+ '__stack_trace',
157
+ () => {
158
+ return this.getStackTraces()
159
+ .map((s) => s.formatted)
160
+ .join('\n\n');
161
+ },
162
+ ],
163
+ [
164
+ '__enhanced_stack_trace',
165
+ (): EnhancedStackTrace => {
166
+ const { sourceMap } = this;
167
+ const sdk: SDKInfo = { name: 'typescript', version: pkg.version };
168
+ const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
169
+ const sources: Record<string, FileSlice[]> = {};
170
+ if (this.showStackTraceSources) {
171
+ for (const { locations } of stacks) {
172
+ for (const { filePath } of locations) {
173
+ if (!filePath) continue;
174
+ const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(filePath)];
175
+ if (!content) continue;
176
+ sources[filePath] = [
177
+ {
178
+ content,
179
+ lineOffset: 0,
180
+ },
181
+ ];
182
+ }
183
+ }
184
+ }
185
+ return { sdk, stacks, sources };
186
+ },
187
+ ],
188
+ ]);
189
+
190
+ /**
191
+ * Loaded in {@link initRuntime}
192
+ */
193
+ public readonly interceptors: Required<WorkflowInterceptors> = { inbound: [], outbound: [], internals: [] };
194
+
195
+ /**
196
+ * Buffer that stores all generated commands, reset after each activation
197
+ */
198
+ protected commands: coresdk.workflow_commands.IWorkflowCommand[] = [];
199
+
200
+ /**
201
+ * Stores all {@link condition}s that haven't been unblocked yet
202
+ */
203
+ public readonly blockedConditions = new Map<number, Condition>();
204
+
205
+ /**
206
+ * Is this Workflow completed?
207
+ *
208
+ * A Workflow will be considered completed if it generates a command that the
209
+ * system considers as a final Workflow command (e.g.
210
+ * completeWorkflowExecution or failWorkflowExecution).
211
+ */
212
+ public completed = false;
213
+
214
+ /**
215
+ * Was this Workflow cancelled?
216
+ */
217
+ protected cancelled = false;
218
+
219
+ /**
220
+ * This is tracked to allow buffering queries until a workflow function is called.
221
+ * TODO(bergundy): I don't think this makes sense since queries run last in an activation and must be responded to in
222
+ * the same activation.
223
+ */
224
+ protected workflowFunctionWasCalled = false;
225
+
226
+ /**
227
+ * The next (incremental) sequence to assign when generating completable commands
228
+ */
229
+ public nextSeqs = {
230
+ timer: 1,
231
+ activity: 1,
232
+ childWorkflow: 1,
233
+ signalWorkflow: 1,
234
+ cancelWorkflow: 1,
235
+ condition: 1,
236
+ // Used internally to keep track of active stack traces
237
+ stack: 1,
238
+ };
239
+
240
+ /**
241
+ * This is set every time the workflow executes an activation
242
+ */
243
+ now: number;
244
+
245
+ /**
246
+ * Reference to the current Workflow, initialized when a Workflow is started
247
+ */
248
+ public workflow?: Workflow;
249
+
250
+ /**
251
+ * Information about the current Workflow
252
+ */
253
+ public readonly info: WorkflowInfo;
254
+
255
+ /**
256
+ * A deterministic RNG, used by the isolate's overridden Math.random
257
+ */
258
+ public random: RNG;
259
+
260
+ public payloadConverter: PayloadConverter = defaultPayloadConverter;
261
+ public failureConverter: FailureConverter = defaultFailureConverter;
262
+
263
+ /**
264
+ * Patches we know the status of for this workflow, as in {@link patched}
265
+ */
266
+ public readonly knownPresentPatches = new Set<string>();
267
+
268
+ /**
269
+ * Patches we sent to core {@link patched}
270
+ */
271
+ public readonly sentPatches = new Set<string>();
272
+
273
+ /**
274
+ * SDK Internal Patches are created by the SDK to avoid breaking history when behaviour
275
+ * of existing API need to be modified.
276
+ */
277
+ public internalPatchNumber = 0;
278
+
279
+ sinkCalls = Array<SinkCall>();
280
+
281
+ constructor({
282
+ info,
283
+ now,
284
+ showStackTraceSources,
285
+ sourceMap,
286
+ randomnessSeed,
287
+ patches,
288
+ }: WorkflowCreateOptionsWithSourceMap) {
289
+ this.info = info;
290
+ this.now = now;
291
+ this.showStackTraceSources = showStackTraceSources;
292
+ this.sourceMap = sourceMap;
293
+ this.random = alea(randomnessSeed);
294
+
295
+ if (info.unsafe.isReplaying) {
296
+ for (const patch of patches) {
297
+ this.knownPresentPatches.add(patch);
298
+ }
299
+ }
300
+ }
301
+
302
+ protected getStackTraces(): Stack[] {
303
+ const { childToParent, promiseToStack } = this.promiseStackStore;
304
+ const internalNodes = [...childToParent.values()].reduce((acc, curr) => {
305
+ for (const p of curr) {
306
+ acc.add(p);
307
+ }
308
+ return acc;
309
+ }, new Set());
310
+ const stacks = new Map<string, Stack>();
311
+ for (const child of childToParent.keys()) {
312
+ if (!internalNodes.has(child)) {
313
+ const stack = promiseToStack.get(child);
314
+ if (!stack || !stack.formatted) continue;
315
+ stacks.set(stack.formatted, stack);
316
+ }
317
+ }
318
+ // Not 100% sure where this comes from, just filter it out
319
+ stacks.delete(' at Promise.then (<anonymous>)');
320
+ stacks.delete(' at Promise.then (<anonymous>)\n');
321
+ return [...stacks].map(([_, stack]) => stack);
322
+ }
323
+
324
+ getAndResetSinkCalls(): SinkCall[] {
325
+ const { sinkCalls } = this;
326
+ this.sinkCalls = [];
327
+ return sinkCalls;
328
+ }
329
+
330
+ /**
331
+ * Buffer a Workflow command to be collected at the end of the current activation.
332
+ *
333
+ * Prevents commands from being added after Workflow completion.
334
+ */
335
+ pushCommand(cmd: coresdk.workflow_commands.IWorkflowCommand, complete = false): void {
336
+ // Only query responses may be sent after completion
337
+ if (this.completed && !cmd.respondToQuery) return;
338
+ this.commands.push(cmd);
339
+ if (complete) {
340
+ this.completed = true;
341
+ }
342
+ }
343
+
344
+ getAndResetCommands(): coresdk.workflow_commands.IWorkflowCommand[] {
345
+ const commands = this.commands;
346
+ this.commands = [];
347
+ return commands;
348
+ }
83
349
 
84
350
  public async startWorkflowNextHandler({ args }: WorkflowExecuteInput): Promise<any> {
85
- const { workflow } = state;
351
+ const { workflow } = this;
86
352
  if (workflow === undefined) {
87
353
  throw new IllegalStateError('Workflow uninitialized');
88
354
  }
@@ -94,7 +360,7 @@ export class Activator implements ActivationHandler {
94
360
  // Otherwise this Workflow will now be queryable.
95
361
  this.workflowFunctionWasCalled = true;
96
362
  // Empty the buffer
97
- const buffer = state.bufferedQueries.splice(0);
363
+ const buffer = this.bufferedQueries.splice(0);
98
364
  for (const activation of buffer) {
99
365
  this.queryWorkflow(activation);
100
366
  }
@@ -103,32 +369,24 @@ export class Activator implements ActivationHandler {
103
369
  }
104
370
 
105
371
  public startWorkflow(activation: coresdk.workflow_activation.IStartWorkflow): void {
106
- const { info } = state;
107
- if (info === undefined) {
108
- throw new IllegalStateError('Workflow has not been initialized');
109
- }
110
- const execute = composeInterceptors(
111
- state.interceptors.inbound,
112
- 'execute',
113
- this.startWorkflowNextHandler.bind(this)
114
- );
372
+ const execute = composeInterceptors(this.interceptors.inbound, 'execute', this.startWorkflowNextHandler.bind(this));
115
373
  untrackPromise(
116
374
  execute({
117
375
  headers: activation.headers ?? {},
118
- args: arrayFromPayloads(state.payloadConverter, activation.arguments),
119
- }).then(completeWorkflow, handleWorkflowFailure)
376
+ args: arrayFromPayloads(this.payloadConverter, activation.arguments),
377
+ }).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this))
120
378
  );
121
379
  }
122
380
 
123
381
  public cancelWorkflow(_activation: coresdk.workflow_activation.ICancelWorkflow): void {
124
- state.cancelled = true;
125
- ROOT_SCOPE.cancel();
382
+ this.cancelled = true;
383
+ this.rootScope.cancel();
126
384
  }
127
385
 
128
386
  public fireTimer(activation: coresdk.workflow_activation.IFireTimer): void {
129
387
  // Timers are a special case where their completion might not be in Workflow state,
130
388
  // this is due to immediate timer cancellation that doesn't go wait for Core.
131
- const completion = maybeConsumeCompletion('timer', getSeq(activation));
389
+ const completion = this.maybeConsumeCompletion('timer', getSeq(activation));
132
390
  completion?.resolve(undefined);
133
391
  }
134
392
 
@@ -136,18 +394,18 @@ export class Activator implements ActivationHandler {
136
394
  if (!activation.result) {
137
395
  throw new TypeError('Got ResolveActivity activation with no result');
138
396
  }
139
- const { resolve, reject } = consumeCompletion('activity', getSeq(activation));
397
+ const { resolve, reject } = this.consumeCompletion('activity', getSeq(activation));
140
398
  if (activation.result.completed) {
141
399
  const completed = activation.result.completed;
142
- const result = completed.result ? state.payloadConverter.fromPayload(completed.result) : undefined;
400
+ const result = completed.result ? this.payloadConverter.fromPayload(completed.result) : undefined;
143
401
  resolve(result);
144
402
  } else if (activation.result.failed) {
145
403
  const { failure } = activation.result.failed;
146
- const err = failure ? state.failureConverter.failureToError(failure) : undefined;
404
+ const err = failure ? this.failureToError(failure) : undefined;
147
405
  reject(err);
148
406
  } else if (activation.result.cancelled) {
149
407
  const { failure } = activation.result.cancelled;
150
- const err = failure ? state.failureConverter.failureToError(failure) : undefined;
408
+ const err = failure ? this.failureToError(failure) : undefined;
151
409
  reject(err);
152
410
  } else if (activation.result.backoff) {
153
411
  reject(new LocalActivityDoBackoff(activation.result.backoff));
@@ -157,7 +415,7 @@ export class Activator implements ActivationHandler {
157
415
  public resolveChildWorkflowExecutionStart(
158
416
  activation: coresdk.workflow_activation.IResolveChildWorkflowExecutionStart
159
417
  ): void {
160
- const { resolve, reject } = consumeCompletion('childWorkflowStart', getSeq(activation));
418
+ const { resolve, reject } = this.consumeCompletion('childWorkflowStart', getSeq(activation));
161
419
  if (activation.succeeded) {
162
420
  resolve(activation.succeeded.runId);
163
421
  } else if (activation.failed) {
@@ -181,7 +439,7 @@ export class Activator implements ActivationHandler {
181
439
  if (!activation.cancelled.failure) {
182
440
  throw new TypeError('Got no failure in cancelled variant');
183
441
  }
184
- reject(state.failureConverter.failureToError(activation.cancelled.failure));
442
+ reject(this.failureToError(activation.cancelled.failure));
185
443
  } else {
186
444
  throw new TypeError('Got ResolveChildWorkflowExecutionStart with no status');
187
445
  }
@@ -191,34 +449,36 @@ export class Activator implements ActivationHandler {
191
449
  if (!activation.result) {
192
450
  throw new TypeError('Got ResolveChildWorkflowExecution activation with no result');
193
451
  }
194
- const { resolve, reject } = consumeCompletion('childWorkflowComplete', getSeq(activation));
452
+ const { resolve, reject } = this.consumeCompletion('childWorkflowComplete', getSeq(activation));
195
453
  if (activation.result.completed) {
196
454
  const completed = activation.result.completed;
197
- const result = completed.result ? state.payloadConverter.fromPayload(completed.result) : undefined;
455
+ const result = completed.result ? this.payloadConverter.fromPayload(completed.result) : undefined;
198
456
  resolve(result);
199
457
  } else if (activation.result.failed) {
200
458
  const { failure } = activation.result.failed;
201
459
  if (failure === undefined || failure === null) {
202
460
  throw new TypeError('Got failed result with no failure attribute');
203
461
  }
204
- reject(state.failureConverter.failureToError(failure));
462
+ reject(this.failureToError(failure));
205
463
  } else if (activation.result.cancelled) {
206
464
  const { failure } = activation.result.cancelled;
207
465
  if (failure === undefined || failure === null) {
208
466
  throw new TypeError('Got cancelled result with no failure attribute');
209
467
  }
210
- reject(state.failureConverter.failureToError(failure));
468
+ reject(this.failureToError(failure));
211
469
  }
212
470
  }
213
471
 
214
472
  // Intentionally not made function async so this handler doesn't show up in the stack trace
215
473
  protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
216
- const fn = state.queryHandlers.get(queryName);
474
+ const fn = this.queryHandlers.get(queryName);
217
475
  if (fn === undefined) {
218
- const knownQueryTypes = [...state.queryHandlers.keys()].join(' ');
476
+ const knownQueryTypes = [...this.queryHandlers.keys()].join(' ');
219
477
  // Fail the query
220
- throw new ReferenceError(
221
- `Workflow did not register a handler for ${queryName}. Registered queries: [${knownQueryTypes}]`
478
+ return Promise.reject(
479
+ new ReferenceError(
480
+ `Workflow did not register a handler for ${queryName}. Registered queries: [${knownQueryTypes}]`
481
+ )
222
482
  );
223
483
  }
224
484
  try {
@@ -234,7 +494,7 @@ export class Activator implements ActivationHandler {
234
494
 
235
495
  public queryWorkflow(activation: coresdk.workflow_activation.IQueryWorkflow): void {
236
496
  if (!this.workflowFunctionWasCalled) {
237
- state.bufferedQueries.push(activation);
497
+ this.bufferedQueries.push(activation);
238
498
  return;
239
499
  }
240
500
 
@@ -244,23 +504,23 @@ export class Activator implements ActivationHandler {
244
504
  }
245
505
 
246
506
  const execute = composeInterceptors(
247
- state.interceptors.inbound,
507
+ this.interceptors.inbound,
248
508
  'handleQuery',
249
509
  this.queryWorkflowNextHandler.bind(this)
250
510
  );
251
511
  execute({
252
512
  queryName: queryType,
253
- args: arrayFromPayloads(state.payloadConverter, activation.arguments),
513
+ args: arrayFromPayloads(this.payloadConverter, activation.arguments),
254
514
  queryId,
255
515
  headers: headers ?? {},
256
516
  }).then(
257
- (result) => completeQuery(queryId, result),
258
- (reason) => failQuery(queryId, reason)
517
+ (result) => this.completeQuery(queryId, result),
518
+ (reason) => this.failQuery(queryId, reason)
259
519
  );
260
520
  }
261
521
 
262
522
  public async signalWorkflowNextHandler({ signalName, args }: SignalInput): Promise<void> {
263
- const fn = state.signalHandlers.get(signalName);
523
+ const fn = this.signalHandlers.get(signalName);
264
524
  if (fn === undefined) {
265
525
  throw new IllegalStateError(`No registered signal handler for signal ${signalName}`);
266
526
  }
@@ -273,33 +533,33 @@ export class Activator implements ActivationHandler {
273
533
  throw new TypeError('Missing activation signalName');
274
534
  }
275
535
 
276
- const fn = state.signalHandlers.get(signalName);
536
+ const fn = this.signalHandlers.get(signalName);
277
537
  if (fn === undefined) {
278
- let buffer = state.bufferedSignals.get(signalName);
538
+ let buffer = this.bufferedSignals.get(signalName);
279
539
  if (buffer === undefined) {
280
540
  buffer = [];
281
- state.bufferedSignals.set(signalName, buffer);
541
+ this.bufferedSignals.set(signalName, buffer);
282
542
  }
283
543
  buffer.push(activation);
284
544
  return;
285
545
  }
286
546
 
287
547
  const execute = composeInterceptors(
288
- state.interceptors.inbound,
548
+ this.interceptors.inbound,
289
549
  'handleSignal',
290
550
  this.signalWorkflowNextHandler.bind(this)
291
551
  );
292
552
  execute({
293
- args: arrayFromPayloads(state.payloadConverter, activation.input),
553
+ args: arrayFromPayloads(this.payloadConverter, activation.input),
294
554
  signalName,
295
555
  headers: headers ?? {},
296
- }).catch(handleWorkflowFailure);
556
+ }).catch(this.handleWorkflowFailure.bind(this));
297
557
  }
298
558
 
299
559
  public resolveSignalExternalWorkflow(activation: coresdk.workflow_activation.IResolveSignalExternalWorkflow): void {
300
- const { resolve, reject } = consumeCompletion('signalWorkflow', getSeq(activation));
560
+ const { resolve, reject } = this.consumeCompletion('signalWorkflow', getSeq(activation));
301
561
  if (activation.failure) {
302
- reject(state.failureConverter.failureToError(activation.failure));
562
+ reject(this.failureToError(activation.failure));
303
563
  } else {
304
564
  resolve(undefined);
305
565
  }
@@ -308,9 +568,9 @@ export class Activator implements ActivationHandler {
308
568
  public resolveRequestCancelExternalWorkflow(
309
569
  activation: coresdk.workflow_activation.IResolveRequestCancelExternalWorkflow
310
570
  ): void {
311
- const { resolve, reject } = consumeCompletion('cancelWorkflow', getSeq(activation));
571
+ const { resolve, reject } = this.consumeCompletion('cancelWorkflow', getSeq(activation));
312
572
  if (activation.failure) {
313
- reject(state.failureConverter.failureToError(activation.failure));
573
+ reject(this.failureToError(activation.failure));
314
574
  } else {
315
575
  resolve(undefined);
316
576
  }
@@ -320,331 +580,127 @@ export class Activator implements ActivationHandler {
320
580
  if (!activation.randomnessSeed) {
321
581
  throw new TypeError('Expected activation with randomnessSeed attribute');
322
582
  }
323
- state.random = alea(activation.randomnessSeed.toBytes());
583
+ this.random = alea(activation.randomnessSeed.toBytes());
324
584
  }
325
585
 
326
586
  public notifyHasPatch(activation: coresdk.workflow_activation.INotifyHasPatch): void {
327
587
  if (!activation.patchId) {
328
588
  throw new TypeError('Notify has patch missing patch name');
329
589
  }
330
- state.knownPresentPatches.add(activation.patchId);
590
+ if (activation.patchId.startsWith('__sdk_internal_patch_number:')) {
591
+ const internalPatchNumber = parseInt(activation.patchId.substring('__sdk_internal_patch_number:'.length));
592
+ if (internalPatchNumber > LATEST_INTERNAL_PATCH_NUMBER)
593
+ throw new IllegalStateError(
594
+ `Unsupported internal patch number: ${internalPatchNumber} > ${LATEST_INTERNAL_PATCH_NUMBER}`
595
+ );
596
+ if (this.internalPatchNumber < internalPatchNumber) this.internalPatchNumber = internalPatchNumber;
597
+ } else {
598
+ this.knownPresentPatches.add(activation.patchId);
599
+ }
600
+ }
601
+
602
+ public checkInternalPatchAtLeast(minimumPatchNumber: number): boolean {
603
+ if (this.internalPatchNumber >= minimumPatchNumber) return true;
604
+ if (!this.info.unsafe.isReplaying) {
605
+ this.internalPatchNumber = minimumPatchNumber;
606
+ this.pushCommand({
607
+ setPatchMarker: { patchId: `__sdk_internal_patch_number:${LATEST_INTERNAL_PATCH_NUMBER}`, deprecated: false },
608
+ });
609
+ return true;
610
+ }
611
+ return false;
331
612
  }
332
613
 
333
614
  public removeFromCache(): void {
334
615
  throw new IllegalStateError('removeFromCache activation job should not reach workflow');
335
616
  }
336
- }
337
-
338
- export type WorkflowsImportFunc = () => Promise<Record<string, any>>;
339
- export type InterceptorsImportFunc = () => Promise<Array<{ interceptors: WorkflowInterceptorsFactory }>>;
340
-
341
- /**
342
- * Keeps all of the Workflow runtime state like pending completions for activities and timers and the scope stack.
343
- *
344
- * State mutates each time the Workflow is activated.
345
- */
346
- export class State {
347
- /**
348
- * Activator executes activation jobs
349
- */
350
- public readonly activator = new Activator();
351
-
352
- /**
353
- * Map of task sequence to a Completion
354
- */
355
- public readonly completions = {
356
- timer: new Map<number, Completion>(),
357
- activity: new Map<number, Completion>(),
358
- childWorkflowStart: new Map<number, Completion>(),
359
- childWorkflowComplete: new Map<number, Completion>(),
360
- signalWorkflow: new Map<number, Completion>(),
361
- cancelWorkflow: new Map<number, Completion>(),
362
- };
363
-
364
- /**
365
- * Holds buffered signal calls until a handler is registered
366
- */
367
- public readonly bufferedSignals = new Map<string, coresdk.workflow_activation.ISignalWorkflow[]>();
368
617
 
369
618
  /**
370
- * Holds buffered query calls until a handler is registered.
371
- *
372
- * **IMPORTANT** queries are only buffered until workflow is started.
373
- * This is required because async interceptors might block workflow function invocation
374
- * which delays query handler registration.
375
- */
376
- public readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
377
-
378
- /**
379
- * Mapping of signal name to handler
619
+ * Transforms failures into a command to be sent to the server.
620
+ * Used to handle any failure emitted by the Workflow.
380
621
  */
381
- public readonly signalHandlers = new Map<string, WorkflowSignalType>();
382
-
383
- /**
384
- * Source map file for looking up the source files in response to __enhanced_stack_trace
385
- */
386
- public sourceMap: RawSourceMap | undefined;
387
-
388
- /**
389
- * Whether or not to send the sources in enhanced stack trace query responses
390
- */
391
- public showStackTraceSources = false;
392
-
393
- protected getStackTraces(): Stack[] {
394
- const { childToParent, promiseToStack } = (globalThis as any).__TEMPORAL__.promiseStackStore as PromiseStackStore;
395
- const internalNodes = [...childToParent.values()].reduce((acc, curr) => {
396
- for (const p of curr) {
397
- acc.add(p);
398
- }
399
- return acc;
400
- }, new Set());
401
- const stacks = new Map<string, Stack>();
402
- for (const child of childToParent.keys()) {
403
- if (!internalNodes.has(child)) {
404
- const stack = promiseToStack.get(child);
405
- if (!stack || !stack.formatted) continue;
406
- stacks.set(stack.formatted, stack);
622
+ async handleWorkflowFailure(error: unknown): Promise<void> {
623
+ if (this.cancelled && isCancellation(error)) {
624
+ this.pushCommand({ cancelWorkflowExecution: {} }, true);
625
+ } else if (error instanceof ContinueAsNew) {
626
+ this.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
627
+ } else {
628
+ if (!(error instanceof TemporalFailure)) {
629
+ // This results in an unhandled rejection which will fail the activation
630
+ // preventing it from completing.
631
+ throw error;
407
632
  }
408
- }
409
- // Not 100% sure where this comes from, just filter it out
410
- stacks.delete(' at Promise.then (<anonymous>)');
411
- stacks.delete(' at Promise.then (<anonymous>)\n');
412
- return [...stacks].map(([_, stack]) => stack);
413
- }
414
-
415
- /**
416
- * Mapping of query name to handler
417
- */
418
- public readonly queryHandlers = new Map<string, WorkflowQueryType>([
419
- [
420
- '__stack_trace',
421
- () => {
422
- return this.getStackTraces()
423
- .map((s) => s.formatted)
424
- .join('\n\n');
425
- },
426
- ],
427
- [
428
- '__enhanced_stack_trace',
429
- (): EnhancedStackTrace => {
430
- const { sourceMap } = this;
431
- const sdk: SDKInfo = { name: 'typescript', version: pkg.version };
432
- const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
433
- const sources: Record<string, FileSlice[]> = {};
434
- if (this.showStackTraceSources) {
435
- for (const { locations } of stacks) {
436
- for (const { filePath } of locations) {
437
- if (!filePath) continue;
438
- const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(filePath)];
439
- if (!content) continue;
440
- sources[filePath] = [
441
- {
442
- content,
443
- lineOffset: 0,
444
- },
445
- ];
446
- }
447
- }
448
- }
449
- return { sdk, stacks, sources };
450
- },
451
- ],
452
- ]);
453
-
454
- /**
455
- * Loaded in {@link initRuntime}
456
- */
457
- public interceptors: Required<WorkflowInterceptors> = { inbound: [], outbound: [], internals: [] };
458
633
 
459
- /**
460
- * Buffer that stores all generated commands, reset after each activation
461
- */
462
- public commands: coresdk.workflow_commands.IWorkflowCommand[] = [];
463
-
464
- /**
465
- * Stores all {@link condition}s that haven't been unblocked yet
466
- */
467
- public blockedConditions = new Map<number, Condition>();
468
-
469
- /**
470
- * Is this Workflow completed?
471
- *
472
- * A Workflow will be considered completed if it generates a command that the
473
- * system considers as a final Workflow command (e.g.
474
- * completeWorkflowExecution or failWorkflowExecution).
475
- */
476
- public completed = false;
477
-
478
- /**
479
- * Was this Workflow cancelled?
480
- */
481
- public cancelled = false;
482
-
483
- /**
484
- * The next (incremental) sequence to assign when generating completable commands
485
- */
486
- public nextSeqs = {
487
- timer: 1,
488
- activity: 1,
489
- childWorkflow: 1,
490
- signalWorkflow: 1,
491
- cancelWorkflow: 1,
492
- condition: 1,
493
- // Used internally to keep track of active stack traces
494
- stack: 1,
495
- };
496
-
497
- /**
498
- * This is set every time the workflow executes an activation
499
- */
500
- #now: number | undefined;
501
-
502
- get now(): number {
503
- if (this.#now === undefined) {
504
- throw new IllegalStateError('Tried to get Date before Workflow has been initialized');
634
+ this.pushCommand(
635
+ {
636
+ failWorkflowExecution: {
637
+ failure: this.errorToFailure(error),
638
+ },
639
+ },
640
+ true
641
+ );
505
642
  }
506
- return this.#now;
507
643
  }
508
644
 
509
- set now(value: number) {
510
- this.#now = value;
645
+ private completeQuery(queryId: string, result: unknown): void {
646
+ this.pushCommand({
647
+ respondToQuery: { queryId, succeeded: { response: this.payloadConverter.toPayload(result) } },
648
+ });
511
649
  }
512
650
 
513
- /**
514
- * Reference to the current Workflow, initialized when a Workflow is started
515
- */
516
- public workflow?: Workflow;
517
-
518
- /**
519
- * Information about the current Workflow
520
- */
521
- public info?: WorkflowInfo;
522
-
523
- /**
524
- * A deterministic RNG, used by the isolate's overridden Math.random
525
- */
526
- public random: RNG = function () {
527
- throw new IllegalStateError('Tried to use Math.random before Workflow has been initialized');
528
- };
529
-
530
- /**
531
- * Used to import the user workflows
532
- *
533
- * Injected on isolate context startup
534
- */
535
- public importWorkflows?: WorkflowsImportFunc;
536
-
537
- /**
538
- * Used to import the user interceptors
539
- *
540
- * Injected on isolate context startup
541
- */
542
- public importInterceptors?: InterceptorsImportFunc;
543
-
544
- public payloadConverter: PayloadConverter = defaultPayloadConverter;
545
- public failureConverter: FailureConverter = defaultFailureConverter;
546
-
547
- /**
548
- * Patches we know the status of for this workflow, as in {@link patched}
549
- */
550
- public readonly knownPresentPatches = new Set<string>();
551
-
552
- /**
553
- * Patches we sent to core {@link patched}
554
- */
555
- public readonly sentPatches = new Set<string>();
556
-
557
- sinkCalls = Array<SinkCall>();
558
-
559
- getAndResetSinkCalls(): SinkCall[] {
560
- const { sinkCalls } = this;
561
- this.sinkCalls = [];
562
- return sinkCalls;
651
+ private failQuery(queryId: string, error: unknown): void {
652
+ this.pushCommand({
653
+ respondToQuery: {
654
+ queryId,
655
+ failed: this.errorToFailure(ensureTemporalFailure(error)),
656
+ },
657
+ });
563
658
  }
564
659
 
565
- /**
566
- * Buffer a Workflow command to be collected at the end of the current activation.
567
- *
568
- * Prevents commands from being added after Workflow completion.
569
- */
570
- pushCommand(cmd: coresdk.workflow_commands.IWorkflowCommand, complete = false): void {
571
- // Only query responses may be sent after completion
572
- if (this.completed && !cmd.respondToQuery) return;
573
- this.commands.push(cmd);
574
- if (complete) {
575
- this.completed = true;
660
+ /** Consume a completion if it exists in Workflow state */
661
+ private maybeConsumeCompletion(type: keyof Activator['completions'], taskSeq: number): Completion | undefined {
662
+ const completion = this.completions[type].get(taskSeq);
663
+ if (completion !== undefined) {
664
+ this.completions[type].delete(taskSeq);
576
665
  }
666
+ return completion;
577
667
  }
578
- }
579
-
580
- export const state = new State();
581
668
 
582
- function completeWorkflow(result: any) {
583
- state.pushCommand(
584
- {
585
- completeWorkflowExecution: {
586
- result: state.payloadConverter.toPayload(result),
587
- },
588
- },
589
- true
590
- );
591
- }
592
-
593
- /**
594
- * Transforms failures into a command to be sent to the server.
595
- * Used to handle any failure emitted by the Workflow.
596
- */
597
- export async function handleWorkflowFailure(error: unknown): Promise<void> {
598
- if (state.cancelled && isCancellation(error)) {
599
- state.pushCommand({ cancelWorkflowExecution: {} }, true);
600
- } else if (error instanceof ContinueAsNew) {
601
- state.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
602
- } else {
603
- if (!(error instanceof TemporalFailure)) {
604
- // This results in an unhandled rejection which will fail the activation
605
- // preventing it from completing.
606
- throw error;
669
+ /** Consume a completion if it exists in Workflow state, throws if it doesn't */
670
+ private consumeCompletion(type: keyof Activator['completions'], taskSeq: number): Completion {
671
+ const completion = this.maybeConsumeCompletion(type, taskSeq);
672
+ if (completion === undefined) {
673
+ throw new IllegalStateError(`No completion for taskSeq ${taskSeq}`);
607
674
  }
675
+ return completion;
676
+ }
608
677
 
609
- state.pushCommand(
678
+ private completeWorkflow(result: unknown): void {
679
+ this.pushCommand(
610
680
  {
611
- failWorkflowExecution: {
612
- failure: state.failureConverter.errorToFailure(error),
681
+ completeWorkflowExecution: {
682
+ result: this.payloadConverter.toPayload(result),
613
683
  },
614
684
  },
615
685
  true
616
686
  );
617
687
  }
618
- }
619
-
620
- function completeQuery(queryId: string, result: unknown) {
621
- state.pushCommand({
622
- respondToQuery: { queryId, succeeded: { response: state.payloadConverter.toPayload(result) } },
623
- });
624
- }
625
688
 
626
- async function failQuery(queryId: string, error: any) {
627
- state.pushCommand({
628
- respondToQuery: { queryId, failed: state.failureConverter.errorToFailure(ensureTemporalFailure(error)) },
629
- });
630
- }
689
+ errorToFailure(err: unknown): ProtoFailure {
690
+ return this.failureConverter.errorToFailure(err, this.payloadConverter);
691
+ }
631
692
 
632
- /** Consume a completion if it exists in Workflow state */
633
- export function maybeConsumeCompletion(type: keyof State['completions'], taskSeq: number): Completion | undefined {
634
- const completion = state.completions[type].get(taskSeq);
635
- if (completion !== undefined) {
636
- state.completions[type].delete(taskSeq);
693
+ failureToError(failure: ProtoFailure): Error {
694
+ return this.failureConverter.failureToError(failure, this.payloadConverter);
637
695
  }
638
- return completion;
639
696
  }
640
697
 
641
- /** Consume a completion if it exists in Workflow state, throws if it doesn't */
642
- export function consumeCompletion(type: keyof State['completions'], taskSeq: number): Completion {
643
- const completion = maybeConsumeCompletion(type, taskSeq);
644
- if (completion === undefined) {
645
- throw new IllegalStateError(`No completion for taskSeq ${taskSeq}`);
698
+ export function getActivator(): Activator {
699
+ const activator = (globalThis as any).__TEMPORAL__?.activator;
700
+ if (activator === undefined) {
701
+ throw new IllegalStateError('Workflow uninitialized');
646
702
  }
647
- return completion;
703
+ return activator;
648
704
  }
649
705
 
650
706
  function getSeq<T extends { seq?: number | null }>(activation: T): number {