@temporalio/workflow 1.11.7 → 1.12.0-rc.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/lib/internals.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Activator = void 0;
7
7
  const common_1 = require("@temporalio/common");
8
+ const payload_search_attributes_1 = require("@temporalio/common/lib/converter/payload-search-attributes");
8
9
  const interceptors_1 = require("@temporalio/common/lib/interceptors");
9
10
  const internal_workflow_1 = require("@temporalio/common/lib/internal-workflow");
10
11
  const alea_1 = require("./alea");
@@ -42,186 +43,263 @@ const [_encodeStartChildWorkflowExecutionFailedCause, decodeStartChildWorkflowEx
42
43
  * - Call user-defined functions, including any form of interceptor.
43
44
  */
44
45
  class Activator {
45
- constructor({ info, now, showStackTraceSources, sourceMap, getTimeOfDay, randomnessSeed, registeredActivityNames, }) {
46
- /**
47
- * Cache for modules - referenced in reusable-vm.ts
48
- */
49
- this.moduleCache = new Map();
50
- /**
51
- * Map of task sequence to a Completion
52
- */
53
- this.completions = {
54
- timer: new Map(),
55
- activity: new Map(),
56
- childWorkflowStart: new Map(),
57
- childWorkflowComplete: new Map(),
58
- signalWorkflow: new Map(),
59
- cancelWorkflow: new Map(),
60
- };
61
- /**
62
- * Holds buffered Update calls until a handler is registered
63
- */
64
- this.bufferedUpdates = Array();
65
- /**
66
- * Holds buffered signal calls until a handler is registered
67
- */
68
- this.bufferedSignals = Array();
69
- /**
70
- * Mapping of update name to handler and validator
71
- */
72
- this.updateHandlers = new Map();
73
- /**
74
- * Mapping of signal name to handler
75
- */
76
- this.signalHandlers = new Map();
77
- /**
78
- * Mapping of in-progress updates to handler execution information.
79
- */
80
- this.inProgressUpdates = new Map();
81
- /**
82
- * Mapping of in-progress signals to handler execution information.
83
- */
84
- this.inProgressSignals = new Map();
85
- /**
86
- * A sequence number providing unique identifiers for signal handler executions.
87
- */
88
- this.signalHandlerExecutionSeq = 0;
89
- this.promiseStackStore = {
90
- promiseToStack: new Map(),
91
- childToParent: new Map(),
92
- };
93
- this.rootScope = new cancellation_scope_1.RootCancellationScope();
94
- /**
95
- * Mapping of query name to handler
96
- */
97
- this.queryHandlers = new Map([
98
- [
99
- '__stack_trace',
100
- {
101
- handler: () => {
102
- return this.getStackTraces()
103
- .map((s) => s.formatted)
104
- .join('\n\n');
105
- },
106
- description: 'Returns a sensible stack trace.',
46
+ /**
47
+ * Cache for modules - referenced in reusable-vm.ts
48
+ */
49
+ moduleCache = new Map();
50
+ /**
51
+ * Map of task sequence to a Completion
52
+ */
53
+ completions = {
54
+ timer: new Map(),
55
+ activity: new Map(),
56
+ childWorkflowStart: new Map(),
57
+ childWorkflowComplete: new Map(),
58
+ signalWorkflow: new Map(),
59
+ cancelWorkflow: new Map(),
60
+ };
61
+ /**
62
+ * Holds buffered Update calls until a handler is registered
63
+ */
64
+ bufferedUpdates = Array();
65
+ /**
66
+ * Holds buffered signal calls until a handler is registered
67
+ */
68
+ bufferedSignals = Array();
69
+ /**
70
+ * Mapping of update name to handler and validator
71
+ */
72
+ updateHandlers = new Map();
73
+ /**
74
+ * Mapping of signal name to handler
75
+ */
76
+ signalHandlers = new Map();
77
+ /**
78
+ * Mapping of in-progress updates to handler execution information.
79
+ */
80
+ inProgressUpdates = new Map();
81
+ /**
82
+ * Mapping of in-progress signals to handler execution information.
83
+ */
84
+ inProgressSignals = new Map();
85
+ /**
86
+ * A sequence number providing unique identifiers for signal handler executions.
87
+ */
88
+ signalHandlerExecutionSeq = 0;
89
+ /**
90
+ * A signal handler that catches calls for non-registered signal names.
91
+ */
92
+ defaultSignalHandler;
93
+ /**
94
+ * A update handler that catches calls for non-registered update names.
95
+ */
96
+ defaultUpdateHandler;
97
+ /**
98
+ * A query handler that catches calls for non-registered query names.
99
+ */
100
+ defaultQueryHandler;
101
+ /**
102
+ * Source map file for looking up the source files in response to __enhanced_stack_trace
103
+ */
104
+ sourceMap;
105
+ /**
106
+ * Whether or not to send the sources in enhanced stack trace query responses
107
+ */
108
+ showStackTraceSources;
109
+ promiseStackStore = {
110
+ promiseToStack: new Map(),
111
+ childToParent: new Map(),
112
+ };
113
+ /**
114
+ * The error that caused the current Workflow Task to fail. Sets if a non-`TemporalFailure`
115
+ * error bubbles up out of the Workflow function, or out of a Signal or Update handler. We
116
+ * capture errors this way because those functions are not technically awaited when started,
117
+ * but left to run asynchronously. There is therefore no real "parent" function that can
118
+ * directly handle those errors, and not capturing it would result in an Unhandled Promise
119
+ * Rejection. So instead, we buffer the error here, to then be processed in the context
120
+ * of our own synchronous Activation handling event loop.
121
+ *
122
+ * Our code does a best effort to stop processing the current activation as soon as possible
123
+ * after this field is set:
124
+ * - If an error is thrown while executing code synchronously (e.g. anything before the
125
+ * first `await` statement in a Workflow function or a signal/update handler), the error
126
+ * will be _immediately_ rethrown, which will prevent execution of further jobs in the
127
+ * current activation. We know we're currently running code synchronously thanks to the
128
+ * `rethrowSynchronously` flag below.
129
+ * - It an error is thrown while executing microtasks, then the error will be rethrown on
130
+ * the next call to `tryUnblockConditions()`.
131
+ *
132
+ * Unfortunately, there's no way for us to prevent further execution of microtasks that have
133
+ * already been scheduled, nor those that will be recursively scheduled from those microtasks.
134
+ * Should more errors get thrown while settling microtasks, those will be ignored (i.e. only
135
+ * the first captured error is preserved).
136
+ */
137
+ workflowTaskError;
138
+ /**
139
+ * Set to true when running synchronous code (e.g. while processing activation jobs and when calling
140
+ * `tryUnblockConditions()`). While this flag is set, it is safe to let errors bubble up.
141
+ */
142
+ rethrowSynchronously = false;
143
+ rootScope = new cancellation_scope_1.RootCancellationScope();
144
+ /**
145
+ * Mapping of query name to handler
146
+ */
147
+ queryHandlers = new Map([
148
+ [
149
+ '__stack_trace',
150
+ {
151
+ handler: () => {
152
+ return this.getStackTraces()
153
+ .map((s) => s.formatted)
154
+ .join('\n\n');
107
155
  },
108
- ],
109
- [
110
- '__enhanced_stack_trace',
111
- {
112
- handler: () => {
113
- const { sourceMap } = this;
114
- const sdk = { name: 'typescript', version: pkg_1.default.version };
115
- const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
116
- const sources = {};
117
- if (this.showStackTraceSources) {
118
- for (const { locations } of stacks) {
119
- for (const { file_path } of locations) {
120
- if (!file_path)
121
- continue;
122
- const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(file_path)];
123
- if (!content)
124
- continue;
125
- sources[file_path] = [
126
- {
127
- line_offset: 0,
128
- content,
129
- },
130
- ];
131
- }
156
+ description: 'Returns a sensible stack trace.',
157
+ },
158
+ ],
159
+ [
160
+ '__enhanced_stack_trace',
161
+ {
162
+ handler: () => {
163
+ const { sourceMap } = this;
164
+ const sdk = { name: 'typescript', version: pkg_1.default.version };
165
+ const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
166
+ const sources = {};
167
+ if (this.showStackTraceSources) {
168
+ for (const { locations } of stacks) {
169
+ for (const { file_path } of locations) {
170
+ if (!file_path)
171
+ continue;
172
+ const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(file_path)];
173
+ if (!content)
174
+ continue;
175
+ sources[file_path] = [
176
+ {
177
+ line_offset: 0,
178
+ content,
179
+ },
180
+ ];
132
181
  }
133
182
  }
134
- return { sdk, stacks, sources };
135
- },
136
- description: 'Returns a stack trace annotated with source information.',
183
+ }
184
+ return { sdk, stacks, sources };
137
185
  },
138
- ],
139
- [
140
- '__temporal_workflow_metadata',
141
- {
142
- handler: () => {
143
- const workflowType = this.info.workflowType;
144
- const queryDefinitions = Array.from(this.queryHandlers.entries()).map(([name, value]) => ({
145
- name,
146
- description: value.description,
147
- }));
148
- const signalDefinitions = Array.from(this.signalHandlers.entries()).map(([name, value]) => ({
149
- name,
150
- description: value.description,
151
- }));
152
- const updateDefinitions = Array.from(this.updateHandlers.entries()).map(([name, value]) => ({
153
- name,
154
- description: value.description,
155
- }));
156
- return {
157
- definition: {
158
- type: workflowType,
159
- queryDefinitions,
160
- signalDefinitions,
161
- updateDefinitions,
162
- },
163
- };
164
- },
165
- description: 'Returns metadata associated with this workflow.',
186
+ description: 'Returns a stack trace annotated with source information.',
187
+ },
188
+ ],
189
+ [
190
+ '__temporal_workflow_metadata',
191
+ {
192
+ handler: () => {
193
+ const workflowType = this.info.workflowType;
194
+ const queryDefinitions = Array.from(this.queryHandlers.entries()).map(([name, value]) => ({
195
+ name,
196
+ description: value.description,
197
+ }));
198
+ const signalDefinitions = Array.from(this.signalHandlers.entries()).map(([name, value]) => ({
199
+ name,
200
+ description: value.description,
201
+ }));
202
+ const updateDefinitions = Array.from(this.updateHandlers.entries()).map(([name, value]) => ({
203
+ name,
204
+ description: value.description,
205
+ }));
206
+ return {
207
+ definition: {
208
+ type: workflowType,
209
+ queryDefinitions,
210
+ signalDefinitions,
211
+ updateDefinitions,
212
+ },
213
+ };
166
214
  },
167
- ],
168
- ]);
169
- /**
170
- * Loaded in {@link initRuntime}
171
- */
172
- this.interceptors = {
173
- inbound: [],
174
- outbound: [],
175
- internals: [],
176
- };
177
- /**
178
- * Buffer that stores all generated commands, reset after each activation
179
- */
180
- this.commands = [];
181
- /**
182
- * Stores all {@link condition}s that haven't been unblocked yet
183
- */
184
- this.blockedConditions = new Map();
185
- /**
186
- * Is this Workflow completed?
187
- *
188
- * A Workflow will be considered completed if it generates a command that the
189
- * system considers as a final Workflow command (e.g.
190
- * completeWorkflowExecution or failWorkflowExecution).
191
- */
192
- this.completed = false;
193
- /**
194
- * Was this Workflow cancelled?
195
- */
196
- this.cancelled = false;
197
- /**
198
- * The next (incremental) sequence to assign when generating completable commands
199
- */
200
- this.nextSeqs = {
201
- timer: 1,
202
- activity: 1,
203
- childWorkflow: 1,
204
- signalWorkflow: 1,
205
- cancelWorkflow: 1,
206
- condition: 1,
207
- // Used internally to keep track of active stack traces
208
- stack: 1,
209
- };
210
- this.payloadConverter = common_1.defaultPayloadConverter;
211
- this.failureConverter = common_1.defaultFailureConverter;
212
- /**
213
- * Patches we know the status of for this workflow, as in {@link patched}
214
- */
215
- this.knownPresentPatches = new Set();
216
- /**
217
- * Patches we sent to core {@link patched}
218
- */
219
- this.sentPatches = new Set();
220
- this.knownFlags = new Set();
221
- /**
222
- * Buffered sink calls per activation
223
- */
224
- this.sinkCalls = Array();
215
+ description: 'Returns metadata associated with this workflow.',
216
+ },
217
+ ],
218
+ ]);
219
+ /**
220
+ * Loaded in {@link initRuntime}
221
+ */
222
+ interceptors = {
223
+ inbound: [],
224
+ outbound: [],
225
+ internals: [],
226
+ };
227
+ /**
228
+ * Buffer that stores all generated commands, reset after each activation
229
+ */
230
+ commands = [];
231
+ /**
232
+ * Stores all {@link condition}s that haven't been unblocked yet
233
+ */
234
+ blockedConditions = new Map();
235
+ /**
236
+ * Is this Workflow completed?
237
+ *
238
+ * A Workflow will be considered completed if it generates a command that the
239
+ * system considers as a final Workflow command (e.g.
240
+ * completeWorkflowExecution or failWorkflowExecution).
241
+ */
242
+ completed = false;
243
+ /**
244
+ * Was this Workflow cancelled?
245
+ */
246
+ cancelled = false;
247
+ /**
248
+ * The next (incremental) sequence to assign when generating completable commands
249
+ */
250
+ nextSeqs = {
251
+ timer: 1,
252
+ activity: 1,
253
+ childWorkflow: 1,
254
+ signalWorkflow: 1,
255
+ cancelWorkflow: 1,
256
+ condition: 1,
257
+ // Used internally to keep track of active stack traces
258
+ stack: 1,
259
+ };
260
+ /**
261
+ * This is set every time the workflow executes an activation
262
+ * May be accessed and modified from outside the VM.
263
+ */
264
+ now;
265
+ /**
266
+ * Reference to the current Workflow, initialized when a Workflow is started
267
+ */
268
+ workflow;
269
+ /**
270
+ * Information about the current Workflow
271
+ * May be accessed from outside the VM.
272
+ */
273
+ info;
274
+ /**
275
+ * A deterministic RNG, used by the isolate's overridden Math.random
276
+ */
277
+ random;
278
+ payloadConverter = common_1.defaultPayloadConverter;
279
+ failureConverter = common_1.defaultFailureConverter;
280
+ /**
281
+ * Patches we know the status of for this workflow, as in {@link patched}
282
+ */
283
+ knownPresentPatches = new Set();
284
+ /**
285
+ * Patches we sent to core {@link patched}
286
+ */
287
+ sentPatches = new Set();
288
+ knownFlags = new Set();
289
+ /**
290
+ * Buffered sink calls per activation
291
+ */
292
+ sinkCalls = Array();
293
+ /**
294
+ * A nanosecond resolution time function, externally injected. This is used to
295
+ * precisely sort logs entries emitted from the Workflow Context vs those emitted
296
+ * from other sources (e.g. main thread, Core, etc).
297
+ */
298
+ getTimeOfDay;
299
+ registeredActivityNames;
300
+ versioningBehavior;
301
+ workflowDefinitionOptionsGetter;
302
+ constructor({ info, now, showStackTraceSources, sourceMap, getTimeOfDay, randomnessSeed, registeredActivityNames, }) {
225
303
  this.getTimeOfDay = getTimeOfDay;
226
304
  this.info = info;
227
305
  this.now = now;
@@ -281,6 +359,7 @@ class Activator {
281
359
  return {
282
360
  commands: this.commands.splice(0),
283
361
  usedInternalFlags: [...this.knownFlags],
362
+ versioningBehavior: this.versioningBehavior,
284
363
  };
285
364
  }
286
365
  async startWorkflowNextHandler({ args }) {
@@ -302,13 +381,17 @@ class Activator {
302
381
  // Most things related to initialization have already been handled in the constructor
303
382
  this.mutateWorkflowInfo((info) => ({
304
383
  ...info,
305
- searchAttributes: (0, common_1.mapFromPayloads)(common_1.searchAttributePayloadConverter, searchAttributes?.indexedFields) ?? {},
384
+ searchAttributes: (0, payload_search_attributes_1.decodeSearchAttributes)(searchAttributes?.indexedFields),
385
+ typedSearchAttributes: (0, payload_search_attributes_1.decodeTypedSearchAttributes)(searchAttributes?.indexedFields),
306
386
  memo: (0, common_1.mapFromPayloads)(this.payloadConverter, memo?.fields),
307
387
  lastResult: (0, common_1.fromPayloadsAtIndex)(this.payloadConverter, 0, lastCompletionResult?.payloads),
308
388
  lastFailure: continuedFailure != null
309
389
  ? this.failureConverter.failureToError(continuedFailure, this.payloadConverter)
310
390
  : undefined,
311
391
  }));
392
+ if (this.workflowDefinitionOptionsGetter) {
393
+ this.versioningBehavior = this.workflowDefinitionOptionsGetter().versioningBehavior;
394
+ }
312
395
  }
313
396
  cancelWorkflow(_activation) {
314
397
  this.cancelled = true;
@@ -393,14 +476,25 @@ class Activator {
393
476
  reject(this.failureToError(failure));
394
477
  }
395
478
  }
479
+ resolveNexusOperationStart(_) {
480
+ throw new Error('TODO');
481
+ }
482
+ resolveNexusOperation(_) {
483
+ throw new Error('TODO');
484
+ }
396
485
  // Intentionally non-async function so this handler doesn't show up in the stack trace
397
486
  queryWorkflowNextHandler({ queryName, args }) {
398
- const fn = this.queryHandlers.get(queryName)?.handler;
487
+ let fn = this.queryHandlers.get(queryName)?.handler;
488
+ if (fn === undefined && this.defaultQueryHandler !== undefined) {
489
+ fn = this.defaultQueryHandler.bind(undefined, queryName);
490
+ }
491
+ // No handler or default registered, fail.
399
492
  if (fn === undefined) {
400
493
  const knownQueryTypes = [...this.queryHandlers.keys()].join(' ');
401
494
  // Fail the query
402
495
  return Promise.reject(new ReferenceError(`Workflow did not register a handler for ${queryName}. Registered queries: [${knownQueryTypes}]`));
403
496
  }
497
+ // Execute handler.
404
498
  try {
405
499
  const ret = fn(...args);
406
500
  if (ret instanceof Promise) {
@@ -436,8 +530,17 @@ class Activator {
436
530
  if (!protocolInstanceId) {
437
531
  throw new TypeError('Missing activation update protocolInstanceId');
438
532
  }
439
- const entry = this.updateHandlers.get(name);
440
- if (!entry) {
533
+ const entry = this.updateHandlers.get(name) ??
534
+ (this.defaultUpdateHandler
535
+ ? {
536
+ handler: this.defaultUpdateHandler.bind(undefined, name),
537
+ validator: undefined,
538
+ // Default to a warning policy.
539
+ unfinishedPolicy: common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON,
540
+ }
541
+ : null);
542
+ // If we don't have an entry from either source, buffer and return
543
+ if (entry === null) {
441
544
  this.bufferedUpdates.push(activation);
442
545
  return;
443
546
  }
@@ -498,7 +601,7 @@ class Activator {
498
601
  this.rejectUpdate(protocolInstanceId, error);
499
602
  }
500
603
  else {
501
- throw error;
604
+ this.handleWorkflowFailure(error);
502
605
  }
503
606
  })
504
607
  .finally(() => this.inProgressUpdates.delete(updateId));
@@ -518,13 +621,22 @@ class Activator {
518
621
  dispatchBufferedUpdates() {
519
622
  const bufferedUpdates = this.bufferedUpdates;
520
623
  while (bufferedUpdates.length) {
521
- const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name));
522
- if (foundIndex === -1) {
523
- // No buffered Updates have a handler yet.
524
- break;
624
+ // We have a default update handler, so all updates are dispatchable.
625
+ if (this.defaultUpdateHandler) {
626
+ const update = bufferedUpdates.shift();
627
+ // Logically, this must be defined as we're in the loop.
628
+ // But Typescript doesn't know that so we use a non-null assertion (!).
629
+ this.doUpdate(update);
630
+ }
631
+ else {
632
+ const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name));
633
+ if (foundIndex === -1) {
634
+ // No buffered Updates have a handler yet.
635
+ break;
636
+ }
637
+ const [update] = bufferedUpdates.splice(foundIndex, 1);
638
+ this.doUpdate(update);
525
639
  }
526
- const [update] = bufferedUpdates.splice(foundIndex, 1);
527
- this.doUpdate(update);
528
640
  }
529
641
  }
530
642
  rejectBufferedUpdates() {
@@ -609,6 +721,8 @@ class Activator {
609
721
  }
610
722
  }
611
723
  warnIfUnfinishedHandlers() {
724
+ if (this.workflowTaskError)
725
+ return;
612
726
  const getWarnable = (handlerExecutions) => {
613
727
  return Array.from(handlerExecutions).filter((ex) => ex.unfinishedPolicy === common_1.HandlerUnfinishedPolicy.WARN_AND_ABANDON);
614
728
  };
@@ -709,19 +823,14 @@ class Activator {
709
823
  * Transforms failures into a command to be sent to the server.
710
824
  * Used to handle any failure emitted by the Workflow.
711
825
  */
712
- async handleWorkflowFailure(error) {
826
+ handleWorkflowFailure(error) {
713
827
  if (this.cancelled && (0, errors_1.isCancellation)(error)) {
714
828
  this.pushCommand({ cancelWorkflowExecution: {} }, true);
715
829
  }
716
830
  else if (error instanceof interfaces_1.ContinueAsNew) {
717
831
  this.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
718
832
  }
719
- else {
720
- if (!(error instanceof common_1.TemporalFailure)) {
721
- // This results in an unhandled rejection which will fail the activation
722
- // preventing it from completing.
723
- throw error;
724
- }
833
+ else if (error instanceof common_1.TemporalFailure) {
725
834
  // Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we
726
835
  // mark all handlers as completed now.
727
836
  this.inProgressSignals.clear();
@@ -732,6 +841,27 @@ class Activator {
732
841
  },
733
842
  }, true);
734
843
  }
844
+ else {
845
+ this.recordWorkflowTaskError(error);
846
+ }
847
+ }
848
+ recordWorkflowTaskError(error) {
849
+ // Only keep the first error that bubbles up; subsequent errors will be ignored.
850
+ if (this.workflowTaskError === undefined)
851
+ this.workflowTaskError = error;
852
+ // Immediately rethrow the error if we know it is safe to do so (i.e. we are not running async
853
+ // microtasks). Otherwise, the error will be rethrown whenever we get an opportunity to do so,
854
+ // e.g. the next time `tryUnblockConditions()` is called.
855
+ if (this.rethrowSynchronously)
856
+ this.maybeRethrowWorkflowTaskError();
857
+ }
858
+ /**
859
+ * If a Workflow Task error was captured, and we are running in synchronous mode,
860
+ * then bubble it up now. This is safe to call even if there is no error to rethrow.
861
+ */
862
+ maybeRethrowWorkflowTaskError() {
863
+ if (this.workflowTaskError)
864
+ throw this.workflowTaskError;
735
865
  }
736
866
  completeQuery(queryId, result) {
737
867
  this.pushCommand({