@temporalio/workflow 1.14.2 → 1.15.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.
Files changed (50) hide show
  1. package/lib/cancellation-scope.d.ts +2 -6
  2. package/lib/cancellation-scope.js +2 -9
  3. package/lib/cancellation-scope.js.map +1 -1
  4. package/lib/flags.js +1 -1
  5. package/lib/flags.js.map +1 -1
  6. package/lib/global-attributes.d.ts +17 -5
  7. package/lib/global-attributes.js +9 -17
  8. package/lib/global-attributes.js.map +1 -1
  9. package/lib/global-overrides.js +6 -6
  10. package/lib/global-overrides.js.map +1 -1
  11. package/lib/index.d.ts +4 -4
  12. package/lib/index.js +2 -1
  13. package/lib/index.js.map +1 -1
  14. package/lib/interceptors.d.ts +16 -1
  15. package/lib/interfaces.d.ts +73 -30
  16. package/lib/interfaces.js +54 -14
  17. package/lib/interfaces.js.map +1 -1
  18. package/lib/internals.d.ts +3 -1
  19. package/lib/internals.js +25 -7
  20. package/lib/internals.js.map +1 -1
  21. package/lib/metrics.js +4 -0
  22. package/lib/metrics.js.map +1 -1
  23. package/lib/nexus.d.ts +51 -0
  24. package/lib/nexus.js +68 -8
  25. package/lib/nexus.js.map +1 -1
  26. package/lib/stack-helpers.js +1 -1
  27. package/lib/stack-helpers.js.map +1 -1
  28. package/lib/update-scope.d.ts +0 -4
  29. package/lib/update-scope.js +0 -7
  30. package/lib/update-scope.js.map +1 -1
  31. package/lib/worker-interface.d.ts +2 -1
  32. package/lib/worker-interface.js +59 -10
  33. package/lib/worker-interface.js.map +1 -1
  34. package/lib/workflow.js +11 -11
  35. package/lib/workflow.js.map +1 -1
  36. package/package.json +4 -4
  37. package/src/cancellation-scope.ts +3 -10
  38. package/src/flags.ts +1 -1
  39. package/src/global-attributes.ts +34 -16
  40. package/src/global-overrides.ts +6 -6
  41. package/src/index.ts +11 -6
  42. package/src/interceptors.ts +18 -1
  43. package/src/interfaces.ts +84 -38
  44. package/src/internals.ts +30 -8
  45. package/src/metrics.ts +7 -0
  46. package/src/nexus.ts +84 -8
  47. package/src/stack-helpers.ts +2 -3
  48. package/src/update-scope.ts +1 -8
  49. package/src/worker-interface.ts +60 -13
  50. package/src/workflow.ts +11 -11
@@ -15,7 +15,8 @@ import {
15
15
  WorkflowExecution,
16
16
  } from '@temporalio/common';
17
17
  import type { coresdk } from '@temporalio/proto';
18
- import { ChildWorkflowOptionsWithDefaults, ContinueAsNewOptions } from './interfaces';
18
+ import type { ChildWorkflowOptionsWithDefaults, ContinueAsNewOptions } from './interfaces';
19
+ import type { NexusOperationCancellationType } from './nexus';
19
20
 
20
21
  export { Next, Headers };
21
22
 
@@ -288,6 +289,22 @@ export interface StartNexusOperationOptions {
288
289
  */
289
290
  readonly scheduleToCloseTimeout?: Duration;
290
291
 
292
+ /**
293
+ * Determines:
294
+ * - whether cancellation requests should be propagated from the Workflow to the Nexus Operation
295
+ * - whether and when should the Operation's cancellation be reported back to the Workflow
296
+ * (i.e. at which moment should the operation's result promise fail with a `NexusOperationFailure`,
297
+ * with `cause` set to a `CancelledFailure`).
298
+ *
299
+ * Note that this setting only applies to cancellation originating from an external request for the
300
+ * Workflow itself, or from internal cancellation of the `CancellationScope` in which the
301
+ * Operation call was made.
302
+ *
303
+ * @default WAIT_CANCELLATION_COMPLETED
304
+ */
305
+ // MAINTENANCE: Keep this typedoc in sync with the `NexusOperationCancellationType` enum
306
+ readonly cancellationType?: NexusOperationCancellationType;
307
+
291
308
  /**
292
309
  * A fixed, single-line summary for this Nexus Operation that may appear in the UI/CLI.
293
310
  * This can be in single-line Temporal markdown format.
package/src/interfaces.ts CHANGED
@@ -45,7 +45,7 @@ export interface WorkflowInfo {
45
45
  * This value may change during the lifetime of an Execution.
46
46
  * @deprecated Use {@link typedSearchAttributes} instead.
47
47
  */
48
- readonly searchAttributes: SearchAttributes; // eslint-disable-line deprecation/deprecation
48
+ readonly searchAttributes: SearchAttributes; // eslint-disable-line @typescript-eslint/no-deprecated
49
49
 
50
50
  /**
51
51
  * Indexed information attached to the Workflow Execution, exposed through an interface.
@@ -208,8 +208,6 @@ export interface WorkflowInfo {
208
208
  * executing this task for the first time and has a Deployment Version set, then its ID will be
209
209
  * used. This value may change over the lifetime of the workflow run, but is deterministic and
210
210
  * safe to use for branching.
211
- *
212
- * @experimental Deployment based versioning is experimental and may change in the future.
213
211
  */
214
212
  readonly currentDeploymentVersion?: WorkerDeploymentVersion;
215
213
 
@@ -235,7 +233,22 @@ export interface UnsafeWorkflowInfo {
235
233
  */
236
234
  readonly now: () => number;
237
235
 
236
+ /**
237
+ * Whether the workflow is currently replaying.
238
+ */
238
239
  readonly isReplaying: boolean;
240
+
241
+ /**
242
+ * Whether the workflow is currently replaying history events.
243
+ *
244
+ * This is similar to {@link isReplaying}, but returns `false` during query handlers and update
245
+ * validators, which are live read-only operations that should not be considered as replaying
246
+ * history events.
247
+ *
248
+ * When this property is true, workflow log messages are suppressed and sinks defined with
249
+ * callDuringReplay=false won't get processed.
250
+ */
251
+ readonly isReplayingHistoryEvents: boolean;
239
252
  }
240
253
 
241
254
  /**
@@ -304,7 +317,7 @@ export interface ContinueAsNewOptions {
304
317
  * Searchable attributes to attach to next Workflow run
305
318
  * @deprecated Use {@link typedSearchAttributes} instead.
306
319
  */
307
- searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation
320
+ searchAttributes?: SearchAttributes; // eslint-disable-line @typescript-eslint/no-deprecated
308
321
  /**
309
322
  * Specifies additional indexed information to attach to the Workflow Execution. More info:
310
323
  * https://docs.temporal.io/docs/typescript/search-attributes
@@ -322,48 +335,74 @@ export interface ContinueAsNewOptions {
322
335
  *
323
336
  * @default 'COMPATIBLE'
324
337
  *
325
- * @deprecated In favor of the new Worker Deployment API.
326
- * @experimental The Worker Versioning API is still being designed. Major changes are expected.
338
+ * @deprecated Worker Versioning is now deprecated. Please use the Worker Deployment API instead: https://docs.temporal.io/worker-deployments
327
339
  */
328
- versioningIntent?: VersioningIntent; // eslint-disable-line deprecation/deprecation
340
+ versioningIntent?: VersioningIntent; // eslint-disable-line @typescript-eslint/no-deprecated
329
341
  }
330
342
 
331
343
  /**
332
- * Specifies:
333
- * - whether cancellation requests are sent to the Child
334
- * - whether and when a {@link CanceledFailure} is thrown from {@link executeChild} or
335
- * {@link ChildWorkflowHandle.result}
344
+ * Determines:
345
+ * - whether cancellation requests should be propagated from the Parent Workflow to the Child, and
346
+ * - whether and when should the Child's cancellation be reported back to the Parent Workflow
347
+ * (i.e. at which moment should the {@link executeChild}'s or {@link ChildWorkflowHandle.result}'s
348
+ * promise fail with a `ChildWorkflowFailure`, with `cause` set to a `CancelledFailure`).
349
+ *
350
+ * Note that this setting only applies to cancellation originating from an external request for the
351
+ * Parent Workflow itself, or from internal cancellation of the `CancellationScope` in which the
352
+ * Child Workflow call was made. Eventual Cancellation of a Child Workflow on completion of the
353
+ * Parent Workflow is controlled by the {@link ParentClosePolicy} setting.
336
354
  *
337
- * @default {@link ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED}
355
+ * @default ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED
338
356
  */
339
- export type ChildWorkflowCancellationType =
340
- (typeof ChildWorkflowCancellationType)[keyof typeof ChildWorkflowCancellationType];
357
+ // MAINTENANCE: Keep this typedoc in sync with the `ChildWorkflowOptions.cancellationType` field
341
358
  export const ChildWorkflowCancellationType = {
342
359
  /**
343
- * Don't send a cancellation request to the Child.
360
+ * Do not propagate cancellation requests to the Child, and immediately report cancellation
361
+ * to the caller.
344
362
  */
345
363
  ABANDON: 'ABANDON',
346
364
 
347
365
  /**
348
- * Send a cancellation request to the Child. Immediately throw the error.
366
+ * Propagate cancellation request from the Parent Workflow to the Child, yet _immediately_ report
367
+ * cancellation to the caller, i.e. without waiting for the server to confirm the cancellation
368
+ * request.
369
+ *
370
+ * Note that this cancellation type provides no guarantee, from the Parent-side, that the
371
+ * cancellation request will actually be atomically added to the Child workflow's history.
372
+ * In particular, the Child may complete (either successfully or uncessfully) before the
373
+ * cancellation is delivered, resulting in a situation where the Parent workflow thinks its child
374
+ * was cancelled, but the child actually completed successfully.
375
+ *
376
+ * To guarantee that the Child will eventually be notified of the cancellation request,
377
+ * use {@link WAIT_CANCELLATION_REQUESTED}.
349
378
  */
350
379
  TRY_CANCEL: 'TRY_CANCEL',
351
380
 
352
381
  /**
353
- * Send a cancellation request to the Child. The Child may respect cancellation, in which case an error will be thrown
354
- * when cancellation has completed, and {@link isCancellation}(error) will be true. On the other hand, the Child may
355
- * ignore the cancellation request, in which case an error might be thrown with a different cause, or the Child may
356
- * complete successfully.
382
+ * Propagate cancellation request from the Parent Workflow to the Child, then wait for the server
383
+ * to confirm that the Child Workflow cancellation request was recorded in its history.
357
384
  *
358
- * @default
385
+ * This cancellation type guarantees that the Child will eventually be notified of the
386
+ * cancellation request (that is, unless the Child terminates inbetween due to unexpected causes).
359
387
  */
360
- WAIT_CANCELLATION_COMPLETED: 'WAIT_CANCELLATION_COMPLETED',
388
+ WAIT_CANCELLATION_REQUESTED: 'WAIT_CANCELLATION_REQUESTED',
361
389
 
362
390
  /**
363
- * Send a cancellation request to the Child. Throw the error once the Server receives the Child cancellation request.
391
+ * Propagate cancellation request from the Parent Workflow to the Child, then wait for completion
392
+ * of the Child Workflow.
393
+ *
394
+ * The Child may respect cancellation, in which case the Parent's `executeChild` or
395
+ * `ChildWorkflowHandle.result` promise will fail with a `ChildWorkflowFailure`, with `cause`
396
+ * set to a `CancelledFailure`. On the other hand, the Child may ignore the cancellation request,
397
+ * in which case the corresponding promise will either resolve with a result (if Child completed
398
+ * successfully) or reject with a different cause (if Child completed uncessfully).
399
+ *
400
+ * @default
364
401
  */
365
- WAIT_CANCELLATION_REQUESTED: 'WAIT_CANCELLATION_REQUESTED',
402
+ WAIT_CANCELLATION_COMPLETED: 'WAIT_CANCELLATION_COMPLETED',
366
403
  } as const;
404
+ export type ChildWorkflowCancellationType =
405
+ (typeof ChildWorkflowCancellationType)[keyof typeof ChildWorkflowCancellationType];
367
406
 
368
407
  // ts-prune-ignore-next
369
408
  export const [encodeChildWorkflowCancellationType, decodeChildWorkflowCancellationType] = makeProtoEnumConverters<
@@ -387,7 +426,6 @@ export const [encodeChildWorkflowCancellationType, decodeChildWorkflowCancellati
387
426
  *
388
427
  * @see {@link https://docs.temporal.io/concepts/what-is-a-parent-close-policy/ | Parent Close Policy}
389
428
  */
390
- export type ParentClosePolicy = (typeof ParentClosePolicy)[keyof typeof ParentClosePolicy];
391
429
  export const ParentClosePolicy = {
392
430
  /**
393
431
  * When the Parent is Closed, the Child is Terminated.
@@ -413,29 +451,30 @@ export const ParentClosePolicy = {
413
451
  *
414
452
  * @deprecated Either leave property `undefined`, or set an explicit policy instead.
415
453
  */
416
- PARENT_CLOSE_POLICY_UNSPECIFIED: undefined, // eslint-disable-line deprecation/deprecation
454
+ PARENT_CLOSE_POLICY_UNSPECIFIED: undefined,
417
455
 
418
456
  /**
419
457
  * When the Parent is Closed, the Child is Terminated.
420
458
  *
421
459
  * @deprecated Use {@link ParentClosePolicy.TERMINATE} instead.
422
460
  */
423
- PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE', // eslint-disable-line deprecation/deprecation
461
+ PARENT_CLOSE_POLICY_TERMINATE: 'TERMINATE',
424
462
 
425
463
  /**
426
464
  * When the Parent is Closed, nothing is done to the Child.
427
465
  *
428
466
  * @deprecated Use {@link ParentClosePolicy.ABANDON} instead.
429
467
  */
430
- PARENT_CLOSE_POLICY_ABANDON: 'ABANDON', // eslint-disable-line deprecation/deprecation
468
+ PARENT_CLOSE_POLICY_ABANDON: 'ABANDON',
431
469
 
432
470
  /**
433
471
  * When the Parent is Closed, the Child is Cancelled.
434
472
  *
435
473
  * @deprecated Use {@link ParentClosePolicy.REQUEST_CANCEL} instead.
436
474
  */
437
- PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'REQUEST_CANCEL', // eslint-disable-line deprecation/deprecation
475
+ PARENT_CLOSE_POLICY_REQUEST_CANCEL: 'REQUEST_CANCEL',
438
476
  } as const;
477
+ export type ParentClosePolicy = (typeof ParentClosePolicy)[keyof typeof ParentClosePolicy];
439
478
 
440
479
  // ts-prune-ignore-next
441
480
  export const [encodeParentClosePolicy, decodeParentClosePolicy] = makeProtoEnumConverters<
@@ -471,19 +510,26 @@ export interface ChildWorkflowOptions extends Omit<CommonWorkflowOptions, 'workf
471
510
  taskQueue?: string;
472
511
 
473
512
  /**
474
- * Specifies:
475
- * - whether cancellation requests are sent to the Child
476
- * - whether and when an error is thrown from {@link executeChild} or
477
- * {@link ChildWorkflowHandle.result}
513
+ * Determines:
514
+ * - whether cancellation requests should be propagated from the Parent Workflow to the Child, and
515
+ * - whether and when should the Child's cancellation be reported back to the Parent Workflow
516
+ * (i.e. at which moment should the {@link executeChild}'s or {@link ChildWorkflowHandle.result}'s
517
+ * promise fail with a `ChildWorkflowFailure`, with `cause` set to a `CancelledFailure`).
518
+ *
519
+ * Note that this setting only applies to cancellation originating from an external request for the
520
+ * Parent Workflow itself, or from internal cancellation of the `CancellationScope` in which the
521
+ * Child Workflow call was made. Eventual Cancellation of a Child Workflow on completion of the
522
+ * Parent Workflow is controlled by the {@link ParentClosePolicy} setting.
478
523
  *
479
- * @default {@link ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED}
524
+ * @default ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED
480
525
  */
526
+ // MAINTENANCE: Keep this typedoc in sync with the `ChildWorkflowCancellationType` enum
481
527
  cancellationType?: ChildWorkflowCancellationType;
482
528
 
483
529
  /**
484
530
  * Specifies how the Child reacts to the Parent Workflow reaching a Closed state.
485
531
  *
486
- * @default {@link ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE}
532
+ * @default ParentClosePolicy.PARENT_CLOSE_POLICY_TERMINATE
487
533
  */
488
534
  parentClosePolicy?: ParentClosePolicy;
489
535
 
@@ -493,10 +539,9 @@ export interface ChildWorkflowOptions extends Omit<CommonWorkflowOptions, 'workf
493
539
  *
494
540
  * @default 'COMPATIBLE'
495
541
  *
496
- * @deprecated In favor of the new Worker Deployment API.
497
- * @experimental The Worker Versioning API is still being designed. Major changes are expected.
542
+ * @deprecated Worker Versioning is now deprecated. Please use the Worker Deployment API instead: https://docs.temporal.io/worker-deployments
498
543
  */
499
- versioningIntent?: VersioningIntent; // eslint-disable-line deprecation/deprecation
544
+ versioningIntent?: VersioningIntent; // eslint-disable-line @typescript-eslint/no-deprecated
500
545
  }
501
546
 
502
547
  export type RequiredChildWorkflowOptions = Required<Pick<ChildWorkflowOptions, 'workflowId' | 'cancellationType'>> & {
@@ -581,6 +626,7 @@ export interface WorkflowCreateOptionsInternal extends WorkflowCreateOptions {
581
626
  sourceMap: RawSourceMap;
582
627
  registeredActivityNames: Set<string>;
583
628
  getTimeOfDay(): bigint;
629
+ stackTracesEnabled: boolean;
584
630
  }
585
631
 
586
632
  /**
package/src/internals.ts CHANGED
@@ -455,6 +455,10 @@ export class Activator implements ActivationHandler {
455
455
  public versioningBehavior?: VersioningBehavior;
456
456
  public workflowDefinitionOptionsGetter?: () => WorkflowDefinitionOptions;
457
457
 
458
+ public readonly workflowSandboxDestructors: (() => void)[] = [];
459
+
460
+ protected readonly stackTracesEnabled: boolean;
461
+
458
462
  constructor({
459
463
  info,
460
464
  now,
@@ -463,6 +467,7 @@ export class Activator implements ActivationHandler {
463
467
  getTimeOfDay,
464
468
  randomnessSeed,
465
469
  registeredActivityNames,
470
+ stackTracesEnabled,
466
471
  }: WorkflowCreateOptionsInternal) {
467
472
  this.getTimeOfDay = getTimeOfDay;
468
473
  this.info = info;
@@ -471,6 +476,7 @@ export class Activator implements ActivationHandler {
471
476
  this.sourceMap = sourceMap;
472
477
  this.random = alea(randomnessSeed);
473
478
  this.registeredActivityNames = registeredActivityNames;
479
+ this.stackTracesEnabled = stackTracesEnabled;
474
480
  }
475
481
 
476
482
  /**
@@ -481,6 +487,9 @@ export class Activator implements ActivationHandler {
481
487
  }
482
488
 
483
489
  protected getStackTraces(): Stack[] {
490
+ if (!this.stackTracesEnabled) {
491
+ throw new IllegalStateError('Workflow stack traces are not enabled on this worker');
492
+ }
484
493
  const { childToParent, promiseToStack } = this.promiseStackStore;
485
494
  const internalNodes = [...childToParent.values()].reduce((acc, curr) => {
486
495
  for (const p of curr) {
@@ -856,12 +865,26 @@ export class Activator implements ActivationHandler {
856
865
  let input: UpdateInput;
857
866
  try {
858
867
  if (runValidator && entry.validator) {
859
- const validate = composeInterceptors(
860
- interceptors,
861
- 'validateUpdate',
862
- this.validateUpdateNextHandler.bind(this, entry.validator)
863
- );
864
- validate(makeInput());
868
+ // Temporarily mark as not replaying history events during validator execution
869
+ // so that logging is permitted. Validators are live read-only operations.
870
+ const wasReplayingHistoryEvents = this.info.unsafe.isReplayingHistoryEvents;
871
+ this.mutateWorkflowInfo((info) => ({
872
+ ...info,
873
+ unsafe: { ...info.unsafe, isReplayingHistoryEvents: false },
874
+ }));
875
+ try {
876
+ const validate = composeInterceptors(
877
+ interceptors,
878
+ 'validateUpdate',
879
+ this.validateUpdateNextHandler.bind(this, entry.validator)
880
+ );
881
+ validate(makeInput());
882
+ } finally {
883
+ this.mutateWorkflowInfo((info) => ({
884
+ ...info,
885
+ unsafe: { ...info.unsafe, isReplayingHistoryEvents: wasReplayingHistoryEvents },
886
+ }));
887
+ }
865
888
  }
866
889
  input = makeInput();
867
890
  } catch (error) {
@@ -928,7 +951,6 @@ export class Activator implements ActivationHandler {
928
951
  const update = this.bufferedUpdates.shift();
929
952
  if (update) {
930
953
  this.rejectUpdate(
931
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
932
954
  update.protocolInstanceId!,
933
955
  ApplicationFailure.nonRetryable(`No registered handler for update: ${update.name}`)
934
956
  );
@@ -995,7 +1017,7 @@ export class Activator implements ActivationHandler {
995
1017
  while (bufferedSignals.length) {
996
1018
  if (this.defaultSignalHandler) {
997
1019
  // We have a default signal handler, so all signals are dispatchable
998
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1020
+
999
1021
  this.signalWorkflow(bufferedSignals.shift()!);
1000
1022
  } else {
1001
1023
  const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName as string));
package/src/metrics.ts CHANGED
@@ -48,6 +48,9 @@ class WorkflowMetricMeterImpl implements MetricMeter {
48
48
  }
49
49
 
50
50
  class WorkflowMetricCounter implements MetricCounter {
51
+ public readonly kind = 'counter';
52
+ public readonly valueType = 'int';
53
+
51
54
  constructor(
52
55
  public readonly name: string,
53
56
  public readonly unit: string | undefined,
@@ -70,6 +73,8 @@ class WorkflowMetricCounter implements MetricCounter {
70
73
  }
71
74
 
72
75
  class WorkflowMetricHistogram implements MetricHistogram {
76
+ public readonly kind = 'histogram';
77
+
73
78
  constructor(
74
79
  public readonly name: string,
75
80
  public readonly valueType: NumericMetricValueType,
@@ -93,6 +98,8 @@ class WorkflowMetricHistogram implements MetricHistogram {
93
98
  }
94
99
 
95
100
  class WorkflowMetricGauge implements MetricGauge {
101
+ public readonly kind = 'gauge';
102
+
96
103
  constructor(
97
104
  public readonly name: string,
98
105
  public readonly valueType: NumericMetricValueType,
package/src/nexus.ts CHANGED
@@ -2,6 +2,8 @@ import * as nexus from 'nexus-rpc';
2
2
  import { msOptionalToTs } from '@temporalio/common/lib/time';
3
3
  import { userMetadataToPayload } from '@temporalio/common/lib/user-metadata';
4
4
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
5
+ import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers';
6
+ import type { coresdk } from '@temporalio/proto';
5
7
  import { CancellationScope } from './cancellation-scope';
6
8
  import { getActivator } from './global-attributes';
7
9
  import { untrackPromise } from './stack-helpers';
@@ -146,17 +148,18 @@ export function createNexusClient<T extends nexus.ServiceDefinition>(options: Ne
146
148
  startNexusOperationNextHandler
147
149
  );
148
150
 
149
- // TODO: Do we want to make the interceptor async like we do for child workflow? That seems redundant.
150
- // REVIEW: I ended up changing this so that the interceptor returns a Promise<StartNexusOperationOutput>,
151
- // and the result promise is contained in that Output object. As a consequence of this,
152
- // the result promise/completion does not exist until the StartNexusOperation event is received.
153
- // That's totally different from what we did in ChildWorkflow, but I think that's cleaner from
154
- // interceptors point of view, and will make it easier to extend the API in the future.
151
+ // The interceptor returns a Promise<StartNexusOperationOutput>, with the result promise contained
152
+ // in that Output object. As a consequence of this, the result promise/completion does not exist
153
+ // until the StartNexusOperation event is received. This is totally different from what we did in
154
+ // ChildWorkflow, but is much cleaner from the interceptors point of view.
155
155
  const { token, result: resultPromise } = await execute({
156
156
  endpoint: options.endpoint,
157
157
  service: options.service.name,
158
158
  operation: opName,
159
- options: operationOptions ?? {},
159
+ options: {
160
+ ...operationOptions,
161
+ cancellationType: operationOptions?.cancellationType ?? 'WAIT_CANCELLATION_COMPLETED',
162
+ },
160
163
  headers: {},
161
164
  seq,
162
165
  input,
@@ -220,7 +223,7 @@ function startNexusOperationNextHandler({
220
223
  nexusHeader: headers,
221
224
  input: activator.payloadConverter.toPayload(input),
222
225
  scheduleToCloseTimeout: msOptionalToTs(options?.scheduleToCloseTimeout),
223
- // FIXME(nexus-post-initial-release): cancellationType is not supported yet
226
+ cancellationType: encodeNexusOperationCancellationType(options?.cancellationType),
224
227
  },
225
228
  userMetadata: userMetadataToPayload(activator.payloadConverter, options?.summary, undefined),
226
229
  });
@@ -231,3 +234,76 @@ function startNexusOperationNextHandler({
231
234
  });
232
235
  });
233
236
  }
237
+
238
+ /**
239
+ * Determines:
240
+ * - whether cancellation requests should be propagated from the Workflow to the Nexus Operation
241
+ * - whether and when should the Operation's cancellation be reported back to the Workflow
242
+ * (i.e. at which moment should the operation's result promise fail with a `NexusOperationFailure`,
243
+ * with `cause` set to a `CancelledFailure`).
244
+ *
245
+ * Note that this setting only applies to cancellation originating from an external request for the
246
+ * Workflow itself, or from internal cancellation of the `CancellationScope` in which the
247
+ * Operation call was made.
248
+ *
249
+ * @experimental Nexus support in Temporal SDK is experimental.
250
+ */
251
+ // MAINTENANCE: Keep this typedoc in sync with the `StartNexusOperationOptions.cancellationType` field
252
+ export const NexusOperationCancellationType = {
253
+ /**
254
+ * Do not propagate cancellation requests to the Nexus Operation, and immediately report
255
+ * cancellation to the caller.
256
+ */
257
+ ABANDON: 'ABANDON',
258
+
259
+ /**
260
+ * Initiate a cancellation request for the Nexus operation and immediately report cancellation to
261
+ * the caller. Note that it doesn't guarantee that cancellation is delivered to the operation if
262
+ * calling workflow exits before the delivery is done. If you want to ensure that cancellation is
263
+ * delivered to the operation, use {@link WAIT_CANCELLATION_REQUESTED}.
264
+ *
265
+ * Propagate cancellation request from the Workflow to the Operation, yet _immediately_ report
266
+ * cancellation to the caller, i.e. without waiting for the server to confirm the cancellation
267
+ * request.
268
+ *
269
+ * Note that this cancellation type provides no guarantee, from the Workflow-side, that the
270
+ * cancellation request will be delivered to the Operation Handler. In particular, either the
271
+ * Operation or the Workflow may complete (either successfully or uncessfully) before the
272
+ * cancellation request is delivered, resulting in a situation where the Operation completed
273
+ * successfully, but the Workflow thinks it was cancelled.
274
+ *
275
+ * To guarantee that the Operation will eventually be notified of the cancellation request,
276
+ * use {@link WAIT_CANCELLATION_REQUESTED}.
277
+ */
278
+ TRY_CANCEL: 'TRY_CANCEL',
279
+
280
+ /**
281
+ * Propagate cancellation request from the Workflow to the Operation, then wait for the server
282
+ * to confirm that the Operation cancellation request was delivered to the Operation Handler.
283
+ */
284
+ WAIT_CANCELLATION_REQUESTED: 'WAIT_CANCELLATION_REQUESTED',
285
+
286
+ /**
287
+ * Propagate cancellation request from the Workflow to the Operation, then wait for completion
288
+ * of the Operation.
289
+ */
290
+ WAIT_CANCELLATION_COMPLETED: 'WAIT_CANCELLATION_COMPLETED',
291
+ } as const;
292
+ export type NexusOperationCancellationType =
293
+ (typeof NexusOperationCancellationType)[keyof typeof NexusOperationCancellationType];
294
+
295
+ const [encodeNexusOperationCancellationType, _] = makeProtoEnumConverters<
296
+ coresdk.nexus.NexusOperationCancellationType,
297
+ typeof coresdk.nexus.NexusOperationCancellationType,
298
+ keyof typeof coresdk.nexus.NexusOperationCancellationType,
299
+ typeof NexusOperationCancellationType,
300
+ ''
301
+ >(
302
+ {
303
+ [NexusOperationCancellationType.WAIT_CANCELLATION_COMPLETED]: 0,
304
+ [NexusOperationCancellationType.ABANDON]: 1,
305
+ [NexusOperationCancellationType.TRY_CANCEL]: 2,
306
+ [NexusOperationCancellationType.WAIT_CANCELLATION_REQUESTED]: 3,
307
+ } as const,
308
+ ''
309
+ );
@@ -1,11 +1,10 @@
1
- import { maybeGetActivatorUntyped } from './global-attributes';
2
- import type { PromiseStackStore } from './internals';
1
+ import { maybeGetActivator } from './global-attributes';
3
2
 
4
3
  /**
5
4
  * Helper function to remove a promise from being tracked for stack trace query purposes
6
5
  */
7
6
  export function untrackPromise(promise: Promise<unknown>): void {
8
- const store = (maybeGetActivatorUntyped() as any)?.promiseStackStore as PromiseStackStore | undefined;
7
+ const store = maybeGetActivator()?.promiseStackStore;
9
8
  if (!store) return;
10
9
  store.childToParent.delete(promise);
11
10
  store.promiseToStack.delete(promise);
@@ -57,11 +57,4 @@ export class UpdateScope {
57
57
  }
58
58
  }
59
59
 
60
- const storage = new AsyncLocalStorage<UpdateScope>();
61
-
62
- /**
63
- * Disable the async local storage for updates.
64
- */
65
- export function disableUpdateStorage(): void {
66
- storage.disable();
67
- }
60
+ const storage: ALS<UpdateScope> = new AsyncLocalStorage();
@@ -6,17 +6,14 @@
6
6
  import { encodeVersioningBehavior, IllegalStateError, WorkflowFunctionWithOptions } from '@temporalio/common';
7
7
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
8
8
  import { coresdk } from '@temporalio/proto';
9
- import { disableStorage } from './cancellation-scope';
10
- import { disableUpdateStorage } from './update-scope';
11
- import { WorkflowInterceptorsFactory } from './interceptors';
12
- import { WorkflowCreateOptionsInternal } from './interfaces';
9
+ import type { WorkflowInterceptorsFactory } from './interceptors';
10
+ import type { WorkflowCreateOptionsInternal } from './interfaces';
13
11
  import { Activator } from './internals';
14
- import { setActivatorUntyped, getActivator } from './global-attributes';
12
+ import { setActivator, getActivator, maybeGetActivator } from './global-attributes';
15
13
 
16
14
  // Export the type for use on the "worker" side
17
15
  export { PromiseStackStore } from './internals';
18
16
 
19
- const global = globalThis as any;
20
17
  const OriginalDate = globalThis.Date;
21
18
 
22
19
  /**
@@ -35,7 +32,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
35
32
  // There's one activator per workflow instance, set it globally on the context.
36
33
  // We do this before importing any user code so user code can statically reference @temporalio/workflow functions
37
34
  // as well as Date and Math.random.
38
- setActivatorUntyped(activator);
35
+ setActivator(activator);
39
36
 
40
37
  activator.rethrowSynchronously = true;
41
38
  try {
@@ -54,7 +51,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
54
51
  activator.failureConverter = customFailureConverter;
55
52
  }
56
53
 
57
- const { importWorkflows, importInterceptors } = global.__TEMPORAL__;
54
+ const { importWorkflows, importInterceptors } = globalThis.__TEMPORAL__;
58
55
  if (importWorkflows === undefined || importInterceptors === undefined) {
59
56
  throw new IllegalStateError('Workflow bundle did not register import hooks');
60
57
  }
@@ -97,6 +94,15 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
97
94
  activator.workflowDefinitionOptionsGetter = activator.workflow.workflowDefinitionOptions;
98
95
  }
99
96
  }
97
+ } catch (e) {
98
+ try {
99
+ // Core won't send an eviction job after an early error, so we are responsible for triggering
100
+ // disposal of the workflow execution context. Otherwise, there might be resource leaks.
101
+ dispose();
102
+ } catch (_innerError) {
103
+ // Ignore error disposing of activator, it is more important to rethrow the original error
104
+ }
105
+ throw e;
100
106
  } finally {
101
107
  activator.rethrowSynchronously = false;
102
108
  }
@@ -252,17 +258,58 @@ export function tryUnblockConditions(): number {
252
258
  }
253
259
  }
254
260
 
261
+ // Handle disposal of the workflow execution context (not to be confused with destroying
262
+ // the sandbox vm, which may possibly be reused if `reuseV8Context` is true).
255
263
  export function dispose(): void {
264
+ let error: unknown | undefined = undefined;
265
+
256
266
  const activator = getActivator();
257
267
  activator.rethrowSynchronously = true;
258
268
  try {
259
- const dispose = composeInterceptors(activator.interceptors.internals, 'dispose', async () => {
260
- disableStorage();
261
- disableUpdateStorage();
262
- });
263
- dispose({});
269
+ try {
270
+ const dispose = composeInterceptors(activator.interceptors.internals, 'dispose', async () => {});
271
+ dispose({});
272
+ } catch (e) {
273
+ error = e;
274
+ }
275
+
276
+ // Destructors are run outside of interceptors, because interceptors themselves often rely
277
+ // on objects that will be destroyed (notably AsyncLocalStorage instances), and because we
278
+ // want to make sure that resources get cleaned up even if an interceptor throws an error.
279
+ // Only the first error (if any) is rethrown.
280
+ for (const destructor of activator.workflowSandboxDestructors.splice(0)) {
281
+ try {
282
+ destructor();
283
+ } catch (e) {
284
+ if (error == null) {
285
+ error = e;
286
+ }
287
+ }
288
+ }
289
+
290
+ if (error != null) throw error;
264
291
  } finally {
265
292
  activator.rethrowSynchronously = false;
293
+
294
+ // The activator is no longer valid past this point.
295
+ setActivator(undefined);
296
+ }
297
+ }
298
+
299
+ // Destroy the sandbox VM (not to be confused with disposing of the workflow execution context).
300
+ export function destroy(): void {
301
+ const activator = maybeGetActivator();
302
+ if (activator) throw new IllegalStateError('Workflow execution context should have been disposed first');
303
+
304
+ let error: unknown | undefined = undefined;
305
+ for (const destructor of globalThis.__temporal_globalSandboxDestructors?.splice(0) ?? []) {
306
+ try {
307
+ destructor();
308
+ } catch (e) {
309
+ if (error == null) {
310
+ error = e;
311
+ }
312
+ }
266
313
  }
267
314
  }
268
315