@temporalio/workflow 1.10.2 → 1.11.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
@@ -6,6 +6,7 @@ import {
6
6
  arrayFromPayloads,
7
7
  defaultPayloadConverter,
8
8
  ensureTemporalFailure,
9
+ HandlerUnfinishedPolicy,
9
10
  IllegalStateError,
10
11
  TemporalFailure,
11
12
  Workflow,
@@ -15,21 +16,24 @@ import {
15
16
  WorkflowUpdateAnnotatedType,
16
17
  ProtoFailure,
17
18
  ApplicationFailure,
19
+ WorkflowUpdateType,
20
+ WorkflowUpdateValidatorType,
18
21
  } from '@temporalio/common';
19
22
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
20
23
  import { checkExtends } from '@temporalio/common/lib/type-helpers';
21
24
  import type { coresdk, temporal } from '@temporalio/proto';
22
25
  import { alea, RNG } from './alea';
23
26
  import { RootCancellationScope } from './cancellation-scope';
27
+ import { UpdateScope } from './update-scope';
24
28
  import { DeterminismViolationError, LocalActivityDoBackoff, isCancellation } from './errors';
25
29
  import { QueryInput, SignalInput, UpdateInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
26
30
  import {
27
31
  ContinueAsNew,
28
32
  DefaultSignalHandler,
29
- SDKInfo,
30
- FileSlice,
33
+ StackTraceSDKInfo,
34
+ StackTraceFileSlice,
31
35
  EnhancedStackTrace,
32
- FileLocation,
36
+ StackTraceFileLocation,
33
37
  WorkflowInfo,
34
38
  WorkflowCreateOptionsInternal,
35
39
  ActivationCompletion,
@@ -37,8 +41,8 @@ import {
37
41
  import { type SinkCall } from './sinks';
38
42
  import { untrackPromise } from './stack-helpers';
39
43
  import pkg from './pkg';
40
- import { executeWithLifecycleLogging } from './logs';
41
44
  import { SdkFlag, assertValidFlag } from './flags';
45
+ import { executeWithLifecycleLogging, log } from './logs';
42
46
 
43
47
  enum StartChildWorkflowExecutionFailedCause {
44
48
  START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED = 0,
@@ -50,7 +54,7 @@ checkExtends<StartChildWorkflowExecutionFailedCause, coresdk.child_workflow.Star
50
54
 
51
55
  export interface Stack {
52
56
  formatted: string;
53
- structured: FileLocation[];
57
+ structured: StackTraceFileLocation[];
54
58
  }
55
59
 
56
60
  /**
@@ -63,11 +67,13 @@ export interface PromiseStackStore {
63
67
 
64
68
  export interface Completion {
65
69
  resolve(val: unknown): unknown;
70
+
66
71
  reject(reason: unknown): unknown;
67
72
  }
68
73
 
69
74
  export interface Condition {
70
75
  fn(): boolean;
76
+
71
77
  resolve(): void;
72
78
  }
73
79
 
@@ -82,10 +88,32 @@ export type ActivationHandler = {
82
88
  [P in keyof coresdk.workflow_activation.IWorkflowActivationJob]: ActivationHandlerFunction<P>;
83
89
  };
84
90
 
91
+ /**
92
+ * Information about an update or signal handler execution.
93
+ */
94
+ interface MessageHandlerExecution {
95
+ name: string;
96
+ unfinishedPolicy: HandlerUnfinishedPolicy;
97
+ id?: string;
98
+ }
99
+
85
100
  /**
86
101
  * Keeps all of the Workflow runtime state like pending completions for activities and timers.
87
102
  *
88
103
  * Implements handlers for all workflow activation jobs.
104
+ *
105
+ * Note that most methods in this class are meant to be called only from within the VM.
106
+ *
107
+ * However, a few methods may be called directly from outside the VM (essentially from `vm-shared.ts`).
108
+ * These methods are specifically marked with a comment and require careful consideration, as the
109
+ * execution context may not properly reflect that of the target workflow execution (e.g.: with Reusable
110
+ * VMs, the `global` may not have been swapped to those of that workflow execution; the active microtask
111
+ * queue may be that of the thread/process, rather than the queue of that VM context; etc). Consequently,
112
+ * methods that are meant to be called from outside of the VM must not do any of the following:
113
+ *
114
+ * - Access any global variable;
115
+ * - Create Promise objects, use async/await, or otherwise schedule microtasks;
116
+ * - Call user-defined functions, including any form of interceptor.
89
117
  */
90
118
  export class Activator implements ActivationHandler {
91
119
  /**
@@ -114,15 +142,6 @@ export class Activator implements ActivationHandler {
114
142
  */
115
143
  readonly bufferedSignals = Array<coresdk.workflow_activation.ISignalWorkflow>();
116
144
 
117
- /**
118
- * Holds buffered query calls until a handler is registered.
119
- *
120
- * **IMPORTANT** queries are only buffered until workflow is started.
121
- * This is required because async interceptors might block workflow function invocation
122
- * which delays query handler registration.
123
- */
124
- protected readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
125
-
126
145
  /**
127
146
  * Mapping of update name to handler and validator
128
147
  */
@@ -133,6 +152,21 @@ export class Activator implements ActivationHandler {
133
152
  */
134
153
  readonly signalHandlers = new Map<string, WorkflowSignalAnnotatedType>();
135
154
 
155
+ /**
156
+ * Mapping of in-progress updates to handler execution information.
157
+ */
158
+ readonly inProgressUpdates = new Map<string, MessageHandlerExecution>();
159
+
160
+ /**
161
+ * Mapping of in-progress signals to handler execution information.
162
+ */
163
+ readonly inProgressSignals = new Map<number, MessageHandlerExecution>();
164
+
165
+ /**
166
+ * A sequence number providing unique identifiers for signal handler executions.
167
+ */
168
+ protected signalHandlerExecutionSeq = 0;
169
+
136
170
  /**
137
171
  * A signal handler that catches calls for non-registered signal names.
138
172
  */
@@ -175,19 +209,19 @@ export class Activator implements ActivationHandler {
175
209
  {
176
210
  handler: (): EnhancedStackTrace => {
177
211
  const { sourceMap } = this;
178
- const sdk: SDKInfo = { name: 'typescript', version: pkg.version };
212
+ const sdk: StackTraceSDKInfo = { name: 'typescript', version: pkg.version };
179
213
  const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
180
- const sources: Record<string, FileSlice[]> = {};
214
+ const sources: Record<string, StackTraceFileSlice[]> = {};
181
215
  if (this.showStackTraceSources) {
182
216
  for (const { locations } of stacks) {
183
- for (const { filePath } of locations) {
184
- if (!filePath) continue;
185
- const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(filePath)];
217
+ for (const { file_path } of locations) {
218
+ if (!file_path) continue;
219
+ const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(file_path)];
186
220
  if (!content) continue;
187
- sources[filePath] = [
221
+ sources[file_path] = [
188
222
  {
223
+ line_offset: 0,
189
224
  content,
190
- lineOffset: 0,
191
225
  },
192
226
  ];
193
227
  }
@@ -218,7 +252,6 @@ export class Activator implements ActivationHandler {
218
252
  return {
219
253
  definition: {
220
254
  type: workflowType,
221
- description: null, // For now, do not set the workflow description in the TS SDK.
222
255
  queryDefinitions,
223
256
  signalDefinitions,
224
257
  updateDefinitions,
@@ -233,7 +266,11 @@ export class Activator implements ActivationHandler {
233
266
  /**
234
267
  * Loaded in {@link initRuntime}
235
268
  */
236
- public readonly interceptors: Required<WorkflowInterceptors> = { inbound: [], outbound: [], internals: [] };
269
+ public readonly interceptors: Required<WorkflowInterceptors> = {
270
+ inbound: [],
271
+ outbound: [],
272
+ internals: [],
273
+ };
237
274
 
238
275
  /**
239
276
  * Buffer that stores all generated commands, reset after each activation
@@ -259,13 +296,6 @@ export class Activator implements ActivationHandler {
259
296
  */
260
297
  protected cancelled = false;
261
298
 
262
- /**
263
- * This is tracked to allow buffering queries until a workflow function is called.
264
- * TODO(bergundy): I don't think this makes sense since queries run last in an activation and must be responded to in
265
- * the same activation.
266
- */
267
- protected workflowFunctionWasCalled = false;
268
-
269
299
  /**
270
300
  * The next (incremental) sequence to assign when generating completable commands
271
301
  */
@@ -282,6 +312,7 @@ export class Activator implements ActivationHandler {
282
312
 
283
313
  /**
284
314
  * This is set every time the workflow executes an activation
315
+ * May be accessed and modified from outside the VM.
285
316
  */
286
317
  now: number;
287
318
 
@@ -292,6 +323,7 @@ export class Activator implements ActivationHandler {
292
323
 
293
324
  /**
294
325
  * Information about the current Workflow
326
+ * May be accessed from outside the VM.
295
327
  */
296
328
  public info: WorkflowInfo;
297
329
 
@@ -333,6 +365,7 @@ export class Activator implements ActivationHandler {
333
365
  showStackTraceSources,
334
366
  sourceMap,
335
367
  getTimeOfDay,
368
+ sdkFlags,
336
369
  randomnessSeed,
337
370
  patches,
338
371
  registeredActivityNames,
@@ -345,13 +378,15 @@ export class Activator implements ActivationHandler {
345
378
  this.random = alea(randomnessSeed);
346
379
  this.registeredActivityNames = registeredActivityNames;
347
380
 
348
- if (info.unsafe.isReplaying) {
349
- for (const patchId of patches) {
350
- this.notifyHasPatch({ patchId });
351
- }
381
+ this.addKnownFlags(sdkFlags);
382
+ for (const patchId of patches) {
383
+ this.notifyHasPatch({ patchId });
352
384
  }
353
385
  }
354
386
 
387
+ /**
388
+ * May be invoked from outside the VM.
389
+ */
355
390
  mutateWorkflowInfo(fn: (info: WorkflowInfo) => WorkflowInfo): void {
356
391
  this.info = fn(this.info);
357
392
  }
@@ -378,6 +413,9 @@ export class Activator implements ActivationHandler {
378
413
  return [...stacks].map(([_, stack]) => stack);
379
414
  }
380
415
 
416
+ /**
417
+ * May be invoked from outside the VM.
418
+ */
381
419
  getAndResetSinkCalls(): SinkCall[] {
382
420
  const { sinkCalls } = this;
383
421
  this.sinkCalls = [];
@@ -390,8 +428,6 @@ export class Activator implements ActivationHandler {
390
428
  * Prevents commands from being added after Workflow completion.
391
429
  */
392
430
  pushCommand(cmd: coresdk.workflow_commands.IWorkflowCommand, complete = false): void {
393
- // Only query responses may be sent after completion
394
- if (this.completed && !cmd.respondToQuery) return;
395
431
  this.commands.push(cmd);
396
432
  if (complete) {
397
433
  this.completed = true;
@@ -410,19 +446,7 @@ export class Activator implements ActivationHandler {
410
446
  if (workflow === undefined) {
411
447
  throw new IllegalStateError('Workflow uninitialized');
412
448
  }
413
- let promise: Promise<any>;
414
- try {
415
- promise = workflow(...args);
416
- } finally {
417
- // Queries must be handled even if there was an exception when invoking the Workflow function.
418
- this.workflowFunctionWasCalled = true;
419
- // Empty the buffer
420
- const buffer = this.bufferedQueries.splice(0);
421
- for (const activation of buffer) {
422
- this.queryWorkflow(activation);
423
- }
424
- }
425
- return await promise;
449
+ return await workflow(...args);
426
450
  }
427
451
 
428
452
  public startWorkflow(activation: coresdk.workflow_activation.IStartWorkflow): void {
@@ -553,11 +577,6 @@ export class Activator implements ActivationHandler {
553
577
  }
554
578
 
555
579
  public queryWorkflow(activation: coresdk.workflow_activation.IQueryWorkflow): void {
556
- if (!this.workflowFunctionWasCalled) {
557
- this.bufferedQueries.push(activation);
558
- return;
559
- }
560
-
561
580
  const { queryType, queryId, headers } = activation;
562
581
  if (!(queryType && queryId)) {
563
582
  throw new TypeError('Missing query activation attributes');
@@ -590,7 +609,8 @@ export class Activator implements ActivationHandler {
590
609
  if (!protocolInstanceId) {
591
610
  throw new TypeError('Missing activation update protocolInstanceId');
592
611
  }
593
- if (!this.updateHandlers.has(name)) {
612
+ const entry = this.updateHandlers.get(name);
613
+ if (!entry) {
594
614
  this.bufferedUpdates.push(activation);
595
615
  return;
596
616
  }
@@ -629,25 +649,31 @@ export class Activator implements ActivationHandler {
629
649
  //
630
650
  // Note that there is a deliberately unhandled promise rejection below.
631
651
  // These are caught elsewhere and fail the corresponding activation.
632
- let input: UpdateInput;
633
- try {
634
- if (runValidator && this.updateHandlers.get(name)?.validator) {
635
- const validate = composeInterceptors(
636
- this.interceptors.inbound,
637
- 'validateUpdate',
638
- this.validateUpdateNextHandler.bind(this)
639
- );
640
- validate(makeInput());
652
+ const doUpdateImpl = async () => {
653
+ let input: UpdateInput;
654
+ try {
655
+ if (runValidator && entry.validator) {
656
+ const validate = composeInterceptors(
657
+ this.interceptors.inbound,
658
+ 'validateUpdate',
659
+ this.validateUpdateNextHandler.bind(this, entry.validator)
660
+ );
661
+ validate(makeInput());
662
+ }
663
+ input = makeInput();
664
+ } catch (error) {
665
+ this.rejectUpdate(protocolInstanceId, error);
666
+ return;
641
667
  }
642
- input = makeInput();
643
- } catch (error) {
644
- this.rejectUpdate(protocolInstanceId, error);
645
- return;
646
- }
647
- const execute = composeInterceptors(this.interceptors.inbound, 'handleUpdate', this.updateNextHandler.bind(this));
648
- this.acceptUpdate(protocolInstanceId);
649
- untrackPromise(
650
- execute(input)
668
+ this.acceptUpdate(protocolInstanceId);
669
+ const execute = composeInterceptors(
670
+ this.interceptors.inbound,
671
+ 'handleUpdate',
672
+ this.updateNextHandler.bind(this, entry.handler)
673
+ );
674
+ const { unfinishedPolicy } = entry;
675
+ this.inProgressUpdates.set(updateId, { name, unfinishedPolicy, id: updateId });
676
+ const res = execute(input)
651
677
  .then((result) => this.completeUpdate(protocolInstanceId, result))
652
678
  .catch((error) => {
653
679
  if (error instanceof TemporalFailure) {
@@ -656,20 +682,18 @@ export class Activator implements ActivationHandler {
656
682
  throw error;
657
683
  }
658
684
  })
659
- );
685
+ .finally(() => this.inProgressUpdates.delete(updateId));
686
+ untrackPromise(res);
687
+ return res;
688
+ };
689
+ untrackPromise(UpdateScope.updateWithInfo(updateId, name, doUpdateImpl));
660
690
  }
661
691
 
662
- protected async updateNextHandler({ name, args }: UpdateInput): Promise<unknown> {
663
- const entry = this.updateHandlers.get(name);
664
- if (!entry) {
665
- return Promise.reject(new IllegalStateError(`No registered update handler for update: ${name}`));
666
- }
667
- const { handler } = entry;
692
+ protected async updateNextHandler(handler: WorkflowUpdateType, { args }: UpdateInput): Promise<unknown> {
668
693
  return await handler(...args);
669
694
  }
670
695
 
671
- protected validateUpdateNextHandler({ name, args }: UpdateInput): void {
672
- const { validator } = this.updateHandlers.get(name) ?? {};
696
+ protected validateUpdateNextHandler(validator: WorkflowUpdateValidatorType | undefined, { args }: UpdateInput): void {
673
697
  if (validator) {
674
698
  validator(...args);
675
699
  }
@@ -723,6 +747,14 @@ export class Activator implements ActivationHandler {
723
747
  return;
724
748
  }
725
749
 
750
+ // If we fall through to the default signal handler then the unfinished
751
+ // policy is WARN_AND_ABANDON; users currently have no way to silence any
752
+ // ensuing warnings.
753
+ const unfinishedPolicy =
754
+ this.signalHandlers.get(signalName)?.unfinishedPolicy ?? HandlerUnfinishedPolicy.WARN_AND_ABANDON;
755
+
756
+ const signalExecutionNum = this.signalHandlerExecutionSeq++;
757
+ this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy });
726
758
  const execute = composeInterceptors(
727
759
  this.interceptors.inbound,
728
760
  'handleSignal',
@@ -732,7 +764,9 @@ export class Activator implements ActivationHandler {
732
764
  args: arrayFromPayloads(this.payloadConverter, activation.input),
733
765
  signalName,
734
766
  headers: headers ?? {},
735
- }).catch(this.handleWorkflowFailure.bind(this));
767
+ })
768
+ .catch(this.handleWorkflowFailure.bind(this))
769
+ .finally(() => this.inProgressSignals.delete(signalExecutionNum));
736
770
  }
737
771
 
738
772
  public dispatchBufferedSignals(): void {
@@ -771,6 +805,24 @@ export class Activator implements ActivationHandler {
771
805
  }
772
806
  }
773
807
 
808
+ public warnIfUnfinishedHandlers(): void {
809
+ const getWarnable = (handlerExecutions: Iterable<MessageHandlerExecution>): MessageHandlerExecution[] => {
810
+ return Array.from(handlerExecutions).filter(
811
+ (ex) => ex.unfinishedPolicy === HandlerUnfinishedPolicy.WARN_AND_ABANDON
812
+ );
813
+ };
814
+
815
+ const warnableUpdates = getWarnable(this.inProgressUpdates.values());
816
+ if (warnableUpdates.length > 0) {
817
+ log.warn(makeUnfinishedUpdateHandlerMessage(warnableUpdates));
818
+ }
819
+
820
+ const warnableSignals = getWarnable(this.inProgressSignals.values());
821
+ if (warnableSignals.length > 0) {
822
+ log.warn(makeUnfinishedSignalHandlerMessage(warnableSignals));
823
+ }
824
+ }
825
+
774
826
  public updateRandomSeed(activation: coresdk.workflow_activation.IUpdateRandomSeed): void {
775
827
  if (!activation.randomnessSeed) {
776
828
  throw new TypeError('Expected activation with randomnessSeed attribute');
@@ -779,9 +831,9 @@ export class Activator implements ActivationHandler {
779
831
  }
780
832
 
781
833
  public notifyHasPatch(activation: coresdk.workflow_activation.INotifyHasPatch): void {
782
- if (!activation.patchId) {
783
- throw new TypeError('Notify has patch missing patch name');
784
- }
834
+ if (!this.info.unsafe.isReplaying)
835
+ throw new IllegalStateError('Unexpected notifyHasPatch job on non-replay activation');
836
+ if (!activation.patchId) throw new TypeError('notifyHasPatch missing patch id');
785
837
  this.knownPresentPatches.add(activation.patchId);
786
838
  }
787
839
 
@@ -801,7 +853,10 @@ export class Activator implements ActivationHandler {
801
853
  return usePatch;
802
854
  }
803
855
 
804
- // Called early while handling an activation to register known flags
856
+ /**
857
+ * Called early while handling an activation to register known flags.
858
+ * May be invoked from outside the VM.
859
+ */
805
860
  public addKnownFlags(flags: number[]): void {
806
861
  for (const flag of flags) {
807
862
  assertValidFlag(flag);
@@ -809,6 +864,11 @@ export class Activator implements ActivationHandler {
809
864
  }
810
865
  }
811
866
 
867
+ /**
868
+ * Check if a flag is known to the Workflow Execution; if not, enable the flag if workflow
869
+ * is not replaying and the flag is configured to be enabled by default.
870
+ * May be invoked from outside the VM.
871
+ */
812
872
  public hasFlag(flag: SdkFlag): boolean {
813
873
  if (this.knownFlags.has(flag.id)) {
814
874
  return true;
@@ -839,7 +899,10 @@ export class Activator implements ActivationHandler {
839
899
  // preventing it from completing.
840
900
  throw error;
841
901
  }
842
-
902
+ // Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we
903
+ // mark all handlers as completed now.
904
+ this.inProgressSignals.clear();
905
+ this.inProgressUpdates.clear();
843
906
  this.pushCommand(
844
907
  {
845
908
  failWorkflowExecution: {
@@ -930,3 +993,44 @@ function getSeq<T extends { seq?: number | null }>(activation: T): number {
930
993
  }
931
994
  return seq;
932
995
  }
996
+
997
+ function makeUnfinishedUpdateHandlerMessage(handlerExecutions: MessageHandlerExecution[]): string {
998
+ const message = `
999
+ [TMPRL1102] Workflow finished while an update handler was still running. This may have interrupted work that the
1000
+ update handler was doing, and the client that sent the update will receive a 'workflow execution
1001
+ already completed' RPCError instead of the update result. You can wait for all update and signal
1002
+ handlers to complete by using \`await workflow.condition(workflow.allHandlersFinished)\`.
1003
+ Alternatively, if both you and the clients sending the update are okay with interrupting running handlers
1004
+ when the workflow finishes, and causing clients to receive errors, then you can disable this warning by
1005
+ passing an option when setting the handler:
1006
+ \`workflow.setHandler(myUpdate, myUpdateHandler, {unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON});\`.`
1007
+ .replace(/\n/g, ' ')
1008
+ .trim();
1009
+
1010
+ return `${message} The following updates were unfinished (and warnings were not disabled for their handler): ${JSON.stringify(
1011
+ handlerExecutions.map((ex) => ({ name: ex.name, id: ex.id }))
1012
+ )}`;
1013
+ }
1014
+
1015
+ function makeUnfinishedSignalHandlerMessage(handlerExecutions: MessageHandlerExecution[]): string {
1016
+ const message = `
1017
+ [TMPRL1102] Workflow finished while a signal handler was still running. This may have interrupted work that the
1018
+ signal handler was doing. You can wait for all update and signal handlers to complete by using
1019
+ \`await workflow.condition(workflow.allHandlersFinished)\`. Alternatively, if both you and the
1020
+ clients sending the update are okay with interrupting running handlers when the workflow finishes,
1021
+ then you can disable this warning by passing an option when setting the handler:
1022
+ \`workflow.setHandler(mySignal, mySignalHandler, {unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON});\`.`
1023
+
1024
+ .replace(/\n/g, ' ')
1025
+ .trim();
1026
+
1027
+ const names = new Map<string, number>();
1028
+ for (const ex of handlerExecutions) {
1029
+ const count = names.get(ex.name) || 0;
1030
+ names.set(ex.name, count + 1);
1031
+ }
1032
+
1033
+ return `${message} The following signals were unfinished (and warnings were not disabled for their handler): ${JSON.stringify(
1034
+ Array.from(names.entries()).map(([name, count]) => ({ name, count }))
1035
+ )}`;
1036
+ }
@@ -0,0 +1,67 @@
1
+ import type { AsyncLocalStorage as ALS } from 'node:async_hooks';
2
+
3
+ /**
4
+ * Option for constructing a UpdateScope
5
+ */
6
+ export interface UpdateScopeOptions {
7
+ /**
8
+ * A workflow-unique identifier for this update.
9
+ */
10
+ id: string;
11
+
12
+ /**
13
+ * The update type name.
14
+ */
15
+ name: string;
16
+ }
17
+
18
+ // AsyncLocalStorage is injected via vm module into global scope.
19
+ // In case Workflow code is imported in Node.js context, replace with an empty class.
20
+ export const AsyncLocalStorage: new <T>() => ALS<T> = (globalThis as any).AsyncLocalStorage ?? class {};
21
+
22
+ export class UpdateScope {
23
+ /**
24
+ * A workflow-unique identifier for this update.
25
+ */
26
+ public readonly id: string;
27
+
28
+ /**
29
+ * The update type name.
30
+ */
31
+ public readonly name: string;
32
+
33
+ constructor(options: UpdateScopeOptions) {
34
+ this.id = options.id;
35
+ this.name = options.name;
36
+ }
37
+
38
+ /**
39
+ * Activate the scope as current and run the update handler `fn`.
40
+ *
41
+ * @return the result of `fn`
42
+ */
43
+ run<T>(fn: () => Promise<T>): Promise<T> {
44
+ return storage.run(this, fn);
45
+ }
46
+
47
+ /**
48
+ * Get the current "active" update scope.
49
+ */
50
+ static current(): UpdateScope | undefined {
51
+ return storage.getStore();
52
+ }
53
+
54
+ /** Alias to `new UpdateScope({ id, name }).run(fn)` */
55
+ static updateWithInfo<T>(id: string, name: string, fn: () => Promise<T>): Promise<T> {
56
+ return new this({ id, name }).run(fn);
57
+ }
58
+ }
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
+ }