@temporalio/workflow 1.8.6 → 1.9.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
@@ -12,15 +12,18 @@ import {
12
12
  WorkflowExecutionAlreadyStartedError,
13
13
  WorkflowQueryType,
14
14
  WorkflowSignalType,
15
+ WorkflowUpdateType,
15
16
  ProtoFailure,
17
+ WorkflowUpdateValidatorType,
18
+ ApplicationFailure,
16
19
  } from '@temporalio/common';
17
20
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
18
- import { checkExtends, SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
21
+ import { checkExtends } from '@temporalio/common/lib/type-helpers';
19
22
  import type { coresdk } from '@temporalio/proto';
20
23
  import { alea, RNG } from './alea';
21
24
  import { RootCancellationScope } from './cancellation-scope';
22
- import { DeterminismViolationError, isCancellation } from './errors';
23
- import { QueryInput, SignalInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
25
+ import { DeterminismViolationError, LocalActivityDoBackoff, isCancellation } from './errors';
26
+ import { QueryInput, SignalInput, UpdateInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
24
27
  import {
25
28
  ContinueAsNew,
26
29
  DefaultSignalHandler,
@@ -31,10 +34,10 @@ import {
31
34
  WorkflowInfo,
32
35
  WorkflowCreateOptionsInternal,
33
36
  } from './interfaces';
34
- import { SinkCall } from './sinks';
37
+ import { type SinkCall } from './sinks';
35
38
  import { untrackPromise } from './stack-helpers';
36
39
  import pkg from './pkg';
37
- import { maybeGetActivatorUntyped } from './global-attributes';
40
+ import { executeWithLifecycleLogging } from './logs';
38
41
 
39
42
  enum StartChildWorkflowExecutionFailedCause {
40
43
  START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED = 0,
@@ -67,16 +70,6 @@ export interface Condition {
67
70
  resolve(): void;
68
71
  }
69
72
 
70
- /**
71
- * A class that acts as a marker for this special result type
72
- */
73
- @SymbolBasedInstanceOfError('LocalActivityDoBackoff')
74
- export class LocalActivityDoBackoff extends Error {
75
- constructor(public readonly backoff: coresdk.activity_result.IDoBackoff) {
76
- super();
77
- }
78
- }
79
-
80
73
  export type ActivationHandlerFunction<K extends keyof coresdk.workflow_activation.IWorkflowActivationJob> = (
81
74
  activation: NonNullable<coresdk.workflow_activation.IWorkflowActivationJob[K]>
82
75
  ) => void;
@@ -110,6 +103,11 @@ export class Activator implements ActivationHandler {
110
103
  cancelWorkflow: new Map<number, Completion>(),
111
104
  };
112
105
 
106
+ /**
107
+ * Holds buffered Update calls until a handler is registered
108
+ */
109
+ readonly bufferedUpdates = Array<coresdk.workflow_activation.IDoUpdate>();
110
+
113
111
  /**
114
112
  * Holds buffered signal calls until a handler is registered
115
113
  */
@@ -124,6 +122,11 @@ export class Activator implements ActivationHandler {
124
122
  */
125
123
  protected readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
126
124
 
125
+ /**
126
+ * Mapping of update name to handler and validator
127
+ */
128
+ readonly updateHandlers = new Map<string, { handler: WorkflowUpdateType; validator?: WorkflowUpdateValidatorType }>();
129
+
127
130
  /**
128
131
  * Mapping of signal name to handler
129
132
  */
@@ -253,7 +256,7 @@ export class Activator implements ActivationHandler {
253
256
  /**
254
257
  * Information about the current Workflow
255
258
  */
256
- public readonly info: WorkflowInfo;
259
+ public info: WorkflowInfo;
257
260
 
258
261
  /**
259
262
  * A deterministic RNG, used by the isolate's overridden Math.random
@@ -310,6 +313,10 @@ export class Activator implements ActivationHandler {
310
313
  }
311
314
  }
312
315
 
316
+ mutateWorkflowInfo(fn: (info: WorkflowInfo) => WorkflowInfo): void {
317
+ this.info = fn(this.info);
318
+ }
319
+
313
320
  protected getStackTraces(): Stack[] {
314
321
  const { childToParent, promiseToStack } = this.promiseStackStore;
315
322
  const internalNodes = [...childToParent.values()].reduce((acc, curr) => {
@@ -367,8 +374,7 @@ export class Activator implements ActivationHandler {
367
374
  try {
368
375
  promise = workflow(...args);
369
376
  } finally {
370
- // Guarantee this runs even if there was an exception when invoking the Workflow function
371
- // Otherwise this Workflow will now be queryable.
377
+ // Queries must be handled even if there was an exception when invoking the Workflow function.
372
378
  this.workflowFunctionWasCalled = true;
373
379
  // Empty the buffer
374
380
  const buffer = this.bufferedQueries.splice(0);
@@ -381,11 +387,14 @@ export class Activator implements ActivationHandler {
381
387
 
382
388
  public startWorkflow(activation: coresdk.workflow_activation.IStartWorkflow): void {
383
389
  const execute = composeInterceptors(this.interceptors.inbound, 'execute', this.startWorkflowNextHandler.bind(this));
390
+
384
391
  untrackPromise(
385
- execute({
386
- headers: activation.headers ?? {},
387
- args: arrayFromPayloads(this.payloadConverter, activation.arguments),
388
- }).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this))
392
+ executeWithLifecycleLogging(() =>
393
+ execute({
394
+ headers: activation.headers ?? {},
395
+ args: arrayFromPayloads(this.payloadConverter, activation.arguments),
396
+ })
397
+ ).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this))
389
398
  );
390
399
  }
391
400
 
@@ -480,7 +489,7 @@ export class Activator implements ActivationHandler {
480
489
  }
481
490
  }
482
491
 
483
- // Intentionally not made function async so this handler doesn't show up in the stack trace
492
+ // Intentionally non-async function so this handler doesn't show up in the stack trace
484
493
  protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
485
494
  const fn = this.queryHandlers.get(queryName);
486
495
  if (fn === undefined) {
@@ -530,6 +539,128 @@ export class Activator implements ActivationHandler {
530
539
  );
531
540
  }
532
541
 
542
+ public doUpdate(activation: coresdk.workflow_activation.IDoUpdate): void {
543
+ const { id: updateId, protocolInstanceId, name, headers, runValidator } = activation;
544
+ if (!updateId) {
545
+ throw new TypeError('Missing activation update id');
546
+ }
547
+ if (!name) {
548
+ throw new TypeError('Missing activation update name');
549
+ }
550
+ if (!protocolInstanceId) {
551
+ throw new TypeError('Missing activation update protocolInstanceId');
552
+ }
553
+ if (!this.updateHandlers.has(name)) {
554
+ this.bufferedUpdates.push(activation);
555
+ return;
556
+ }
557
+
558
+ const makeInput = (): UpdateInput => ({
559
+ updateId,
560
+ args: arrayFromPayloads(this.payloadConverter, activation.input),
561
+ name,
562
+ headers: headers ?? {},
563
+ });
564
+
565
+ // The implementation below is responsible for upholding, and constrained
566
+ // by, the following contract:
567
+ //
568
+ // 1. If no validator is present then validation interceptors will not be run.
569
+ //
570
+ // 2. During validation, any error must fail the Update; during the Update
571
+ // itself, Temporal errors fail the Update whereas other errors fail the
572
+ // activation.
573
+ //
574
+ // 3. The handler must not see any mutations of the arguments made by the
575
+ // validator.
576
+ //
577
+ // 4. Any error when decoding/deserializing input must be caught and result
578
+ // in rejection of the Update before it is accepted, even if there is no
579
+ // validator.
580
+ //
581
+ // 5. The initial synchronous portion of the (async) Update handler should
582
+ // be executed after the (sync) validator completes such that there is
583
+ // minimal opportunity for a different concurrent task to be scheduled
584
+ // between them.
585
+ //
586
+ // 6. The stack trace view provided in the Temporal UI must not be polluted
587
+ // by promises that do not derive from user code. This implies that
588
+ // async/await syntax may not be used.
589
+ //
590
+ // Note that there is a deliberately unhandled promise rejection below.
591
+ // These are caught elsewhere and fail the corresponding activation.
592
+ let input: UpdateInput;
593
+ try {
594
+ if (runValidator && this.updateHandlers.get(name)?.validator) {
595
+ const validate = composeInterceptors(
596
+ this.interceptors.inbound,
597
+ 'validateUpdate',
598
+ this.validateUpdateNextHandler.bind(this)
599
+ );
600
+ validate(makeInput());
601
+ }
602
+ input = makeInput();
603
+ } catch (error) {
604
+ this.rejectUpdate(protocolInstanceId, error);
605
+ return;
606
+ }
607
+ const execute = composeInterceptors(this.interceptors.inbound, 'handleUpdate', this.updateNextHandler.bind(this));
608
+ this.acceptUpdate(protocolInstanceId);
609
+ untrackPromise(
610
+ execute(input)
611
+ .then((result) => this.completeUpdate(protocolInstanceId, result))
612
+ .catch((error) => {
613
+ if (error instanceof TemporalFailure) {
614
+ this.rejectUpdate(protocolInstanceId, error);
615
+ } else {
616
+ throw error;
617
+ }
618
+ })
619
+ );
620
+ }
621
+
622
+ protected async updateNextHandler({ name, args }: UpdateInput): Promise<unknown> {
623
+ const entry = this.updateHandlers.get(name);
624
+ if (!entry) {
625
+ return Promise.reject(new IllegalStateError(`No registered update handler for update: ${name}`));
626
+ }
627
+ const { handler } = entry;
628
+ return await handler(...args);
629
+ }
630
+
631
+ protected validateUpdateNextHandler({ name, args }: UpdateInput): void {
632
+ const { validator } = this.updateHandlers.get(name) ?? {};
633
+ if (validator) {
634
+ validator(...args);
635
+ }
636
+ }
637
+
638
+ public dispatchBufferedUpdates(): void {
639
+ const bufferedUpdates = this.bufferedUpdates;
640
+ while (bufferedUpdates.length) {
641
+ const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name as string));
642
+ if (foundIndex === -1) {
643
+ // No buffered Updates have a handler yet.
644
+ break;
645
+ }
646
+ const [update] = bufferedUpdates.splice(foundIndex, 1);
647
+ this.doUpdate(update);
648
+ }
649
+ }
650
+
651
+ public rejectBufferedUpdates(): void {
652
+ while (this.bufferedUpdates.length) {
653
+ const update = this.bufferedUpdates.shift();
654
+ if (update) {
655
+ this.rejectUpdate(
656
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
657
+ update.protocolInstanceId!,
658
+ ApplicationFailure.nonRetryable(`No registered handler for update: ${update.name}`)
659
+ );
660
+ }
661
+ }
662
+ }
663
+
533
664
  public async signalWorkflowNextHandler({ signalName, args }: SignalInput): Promise<void> {
534
665
  const fn = this.signalHandlers.get(signalName);
535
666
  if (fn) {
@@ -537,7 +668,7 @@ export class Activator implements ActivationHandler {
537
668
  } else if (this.defaultSignalHandler) {
538
669
  return await this.defaultSignalHandler(signalName, ...args);
539
670
  } else {
540
- throw new IllegalStateError(`No registered signal handler for signal ${signalName}`);
671
+ throw new IllegalStateError(`No registered signal handler for signal: ${signalName}`);
541
672
  }
542
673
  }
543
674
 
@@ -572,7 +703,7 @@ export class Activator implements ActivationHandler {
572
703
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
573
704
  this.signalWorkflow(bufferedSignals.shift()!);
574
705
  } else {
575
- const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName ?? ''));
706
+ const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName as string));
576
707
  if (foundIndex === -1) break;
577
708
  const [signal] = bufferedSignals.splice(foundIndex, 1);
578
709
  this.signalWorkflow(signal);
@@ -660,6 +791,25 @@ export class Activator implements ActivationHandler {
660
791
  });
661
792
  }
662
793
 
794
+ private acceptUpdate(protocolInstanceId: string): void {
795
+ this.pushCommand({ updateResponse: { protocolInstanceId, accepted: {} } });
796
+ }
797
+
798
+ private completeUpdate(protocolInstanceId: string, result: unknown): void {
799
+ this.pushCommand({
800
+ updateResponse: { protocolInstanceId, completed: this.payloadConverter.toPayload(result) },
801
+ });
802
+ }
803
+
804
+ private rejectUpdate(protocolInstanceId: string, error: unknown): void {
805
+ this.pushCommand({
806
+ updateResponse: {
807
+ protocolInstanceId,
808
+ rejected: this.errorToFailure(ensureTemporalFailure(error)),
809
+ },
810
+ });
811
+ }
812
+
663
813
  /** Consume a completion if it exists in Workflow state */
664
814
  private maybeConsumeCompletion(type: keyof Activator['completions'], taskSeq: number): Completion | undefined {
665
815
  const completion = this.completions[type].get(taskSeq);
@@ -698,18 +848,6 @@ export class Activator implements ActivationHandler {
698
848
  }
699
849
  }
700
850
 
701
- export function maybeGetActivator(): Activator | undefined {
702
- return maybeGetActivatorUntyped() as Activator | undefined;
703
- }
704
-
705
- export function getActivator(): Activator {
706
- const activator = maybeGetActivator();
707
- if (activator === undefined) {
708
- throw new IllegalStateError('Workflow uninitialized');
709
- }
710
- return activator;
711
- }
712
-
713
851
  function getSeq<T extends { seq?: number | null }>(activation: T): number {
714
852
  const seq = activation.seq;
715
853
  if (seq === undefined || seq === null) {
package/src/logs.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { composeInterceptors } from '@temporalio/common/lib/interceptors';
2
+ import { untrackPromise } from './stack-helpers';
3
+ import { type Sink, type Sinks, proxySinks } from './sinks';
4
+ import { isCancellation } from './errors';
5
+ import { WorkflowInfo, ContinueAsNew } from './interfaces';
6
+ import { assertInWorkflowContext } from './global-attributes';
7
+
8
+ export interface WorkflowLogger extends Sink {
9
+ trace(message: string, attrs?: Record<string, unknown>): void;
10
+ debug(message: string, attrs?: Record<string, unknown>): void;
11
+ info(message: string, attrs?: Record<string, unknown>): void;
12
+ warn(message: string, attrs?: Record<string, unknown>): void;
13
+ error(message: string, attrs?: Record<string, unknown>): void;
14
+ }
15
+
16
+ /**
17
+ * Sink interface for forwarding logs from the Workflow sandbox to the Worker
18
+ *
19
+ * @deprecated Do not use LoggerSinks directly. To log from Workflow code, use the `log` object
20
+ * exported by the `@temporalio/workflow` package. To capture log messages emitted
21
+ * by Workflow code, set the {@link Runtime.logger} property.
22
+ */
23
+ export interface LoggerSinksDeprecated extends Sinks {
24
+ /**
25
+ * @deprecated Do not use LoggerSinks directly. To log from Workflow code, use the `log` object
26
+ * exported by the `@temporalio/workflow` package. To capture log messages emitted
27
+ * by Workflow code, set the {@link Runtime.logger} property.
28
+ */
29
+ defaultWorkerLogger: WorkflowLogger;
30
+ }
31
+
32
+ /**
33
+ * Sink interface for forwarding logs from the Workflow sandbox to the Worker
34
+ */
35
+ export interface LoggerSinksInternal extends Sinks {
36
+ __temporal_logger: WorkflowLogger;
37
+ }
38
+
39
+ const loggerSink = proxySinks<LoggerSinksInternal>().__temporal_logger;
40
+
41
+ /**
42
+ * Symbol used by the SDK logger to extract a timestamp from log attributes.
43
+ * Also defined in `worker/logger.ts` - intentionally not shared.
44
+ */
45
+ const LogTimestamp = Symbol.for('log_timestamp');
46
+
47
+ /**
48
+ * Default workflow logger.
49
+ *
50
+ * This logger is replay-aware and will omit log messages on workflow replay. Messages emitted by this logger are
51
+ * funnelled through a sink that forwards them to the logger registered on {@link Runtime.logger}.
52
+ *
53
+ * Notice that since sinks are used to power this logger, any log attributes must be transferable via the
54
+ * {@link https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist | postMessage}
55
+ * API.
56
+ *
57
+ * NOTE: Specifying a custom logger through {@link defaultSink} or by manually registering a sink named
58
+ * `defaultWorkerLogger` has been deprecated. Please use {@link Runtime.logger} instead.
59
+ */
60
+ export const log: WorkflowLogger = Object.fromEntries(
61
+ (['trace', 'debug', 'info', 'warn', 'error'] as Array<keyof WorkflowLogger>).map((level) => {
62
+ return [
63
+ level,
64
+ (message: string, attrs?: Record<string, unknown>) => {
65
+ const activator = assertInWorkflowContext('Workflow.log(...) may only be used from workflow context.');
66
+ const getLogAttributes = composeInterceptors(activator.interceptors.outbound, 'getLogAttributes', (a) => a);
67
+ return loggerSink[level](message, {
68
+ // Inject the call time in nanosecond resolution as expected by the worker logger.
69
+ [LogTimestamp]: activator.getTimeOfDay(),
70
+ ...getLogAttributes(workflowLogAttributes(activator.info)),
71
+ ...attrs,
72
+ });
73
+ },
74
+ ];
75
+ })
76
+ ) as any;
77
+
78
+ export function executeWithLifecycleLogging(fn: () => Promise<unknown>): Promise<unknown> {
79
+ log.debug('Workflow started');
80
+ const p = fn().then(
81
+ (res) => {
82
+ log.debug('Workflow completed');
83
+ return res;
84
+ },
85
+ (error) => {
86
+ // Avoid using instanceof checks in case the modules they're defined in loaded more than once,
87
+ // e.g. by jest or when multiple versions are installed.
88
+ if (typeof error === 'object' && error != null) {
89
+ if (isCancellation(error)) {
90
+ log.debug('Workflow completed as cancelled');
91
+ throw error;
92
+ } else if (error instanceof ContinueAsNew) {
93
+ log.debug('Workflow continued as new');
94
+ throw error;
95
+ }
96
+ }
97
+ log.warn('Workflow failed', { error });
98
+ throw error;
99
+ }
100
+ );
101
+ // Avoid showing this interceptor in stack trace query
102
+ untrackPromise(p);
103
+ return p;
104
+ }
105
+
106
+ /**
107
+ * Returns a map of attributes to be set _by default_ on log messages for a given Workflow.
108
+ * Note that this function may be called from outside of the Workflow context (eg. by the worker itself).
109
+ */
110
+ export function workflowLogAttributes(info: WorkflowInfo): Record<string, unknown> {
111
+ return {
112
+ namespace: info.namespace,
113
+ taskQueue: info.taskQueue,
114
+ workflowId: info.workflowId,
115
+ runId: info.runId,
116
+ workflowType: info.workflowType,
117
+ };
118
+ }
package/src/sinks.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { WorkflowInfo } from './interfaces';
18
+ import { assertInWorkflowContext } from './global-attributes';
18
19
 
19
20
  /**
20
21
  * Any function signature can be used for Sink functions as long as the return type is `void`.
@@ -44,14 +45,64 @@ export interface SinkCall {
44
45
  }
45
46
 
46
47
  /**
47
- * Sink interface for forwarding logs from the Workflow sandbox to the Worker
48
+ * Get a reference to Sinks for exporting data out of the Workflow.
49
+ *
50
+ * These Sinks **must** be registered with the Worker in order for this
51
+ * mechanism to work.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { proxySinks, Sinks } from '@temporalio/workflow';
56
+ *
57
+ * interface MySinks extends Sinks {
58
+ * logger: {
59
+ * info(message: string): void;
60
+ * error(message: string): void;
61
+ * };
62
+ * }
63
+ *
64
+ * const { logger } = proxySinks<MyDependencies>();
65
+ * logger.info('setting up');
66
+ *
67
+ * export function myWorkflow() {
68
+ * return {
69
+ * async execute() {
70
+ * logger.info("hey ho");
71
+ * logger.error("lets go");
72
+ * }
73
+ * };
74
+ * }
75
+ * ```
48
76
  */
49
- export interface LoggerSinks extends Sinks {
50
- defaultWorkerLogger: {
51
- trace(message: string, attrs?: Record<string, unknown>): void;
52
- debug(message: string, attrs?: Record<string, unknown>): void;
53
- info(message: string, attrs?: Record<string, unknown>): void;
54
- warn(message: string, attrs?: Record<string, unknown>): void;
55
- error(message: string, attrs?: Record<string, unknown>): void;
56
- };
77
+ export function proxySinks<T extends Sinks>(): T {
78
+ return new Proxy(
79
+ {},
80
+ {
81
+ get(_, ifaceName) {
82
+ return new Proxy(
83
+ {},
84
+ {
85
+ get(_, fnName) {
86
+ return (...args: any[]) => {
87
+ const activator = assertInWorkflowContext(
88
+ 'Proxied sinks functions may only be used from a Workflow Execution.'
89
+ );
90
+ activator.sinkCalls.push({
91
+ ifaceName: ifaceName as string,
92
+ fnName: fnName as string,
93
+ // Sink function doesn't get called immediately. Make a clone of the sink's args, so that further mutations
94
+ // to these objects don't corrupt the args that the sink function will receive. Only available from node 17.
95
+ args: (globalThis as any).structuredClone ? (globalThis as any).structuredClone(args) : args,
96
+ // activator.info is internally copy-on-write. This ensure that any further mutations
97
+ // to the workflow state in the context of the present activation will not corrupt the
98
+ // workflowInfo state that gets passed when the sink function actually gets called.
99
+ workflowInfo: activator.info,
100
+ });
101
+ };
102
+ },
103
+ }
104
+ );
105
+ },
106
+ }
107
+ ) as any;
57
108
  }
@@ -6,14 +6,14 @@
6
6
  import { IllegalStateError } from '@temporalio/common';
7
7
  import { msToTs, tsToMs } from '@temporalio/common/lib/time';
8
8
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
9
- import type { coresdk } from '@temporalio/proto';
9
+ import { coresdk } from '@temporalio/proto';
10
10
  import { disableStorage } from './cancellation-scope';
11
11
  import { DeterminismViolationError } from './errors';
12
12
  import { WorkflowInterceptorsFactory } from './interceptors';
13
- import { WorkflowCreateOptionsInternal, WorkflowInfo } from './interfaces';
14
- import { Activator, getActivator } from './internals';
15
- import { SinkCall } from './sinks';
16
- import { setActivatorUntyped } from './global-attributes';
13
+ import { WorkflowCreateOptionsInternal } from './interfaces';
14
+ import { Activator } from './internals';
15
+ import { setActivatorUntyped, getActivator } from './global-attributes';
16
+ import { type SinkCall } from './sinks';
17
17
 
18
18
  // Export the type for use on the "worker" side
19
19
  export { PromiseStackStore } from './internals';
@@ -93,9 +93,13 @@ export function overrideGlobals(): void {
93
93
  * Sets required internal state and instantiates the workflow and interceptors.
94
94
  */
95
95
  export function initRuntime(options: WorkflowCreateOptionsInternal): void {
96
- const info: WorkflowInfo = fixPrototypes(options.info);
97
- info.unsafe.now = OriginalDate.now;
98
- const activator = new Activator({ ...options, info });
96
+ const activator = new Activator({
97
+ ...options,
98
+ info: fixPrototypes({
99
+ ...options.info,
100
+ unsafe: { ...options.info.unsafe, now: OriginalDate.now },
101
+ }),
102
+ });
99
103
  // There's on activator per workflow instance, set it globally on the context.
100
104
  // We do this before importing any user code so user code can statically reference @temporalio/workflow functions
101
105
  // as well as Date and Math.random.
@@ -136,7 +140,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
136
140
  }
137
141
 
138
142
  const mod = importWorkflows();
139
- const workflowFn = mod[info.workflowType];
143
+ const workflowFn = mod[activator.info.workflowType];
140
144
  const defaultWorkflowFn = mod['default'];
141
145
 
142
146
  if (typeof workflowFn === 'function') {
@@ -148,7 +152,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
148
152
  workflowFn === undefined
149
153
  ? 'no such function is exported by the workflow bundle'
150
154
  : `expected a function, but got: '${typeof workflowFn}'`;
151
- throw new TypeError(`Failed to initialize workflow of type '${info.workflowType}': ${details}`);
155
+ throw new TypeError(`Failed to initialize workflow of type '${activator.info.workflowType}': ${details}`);
152
156
  }
153
157
  }
154
158
 
@@ -188,12 +192,19 @@ export function activate(activation: coresdk.workflow_activation.WorkflowActivat
188
192
  }
189
193
 
190
194
  // The Rust Core ensures that these activation fields are not null
191
- activator.info.unsafe.isReplaying = activation.isReplaying ?? false;
192
- activator.info.historyLength = activation.historyLength as number;
193
- // Exact truncation for multi-petabyte histories
194
- // historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
195
- activator.info.historySize = activation.historySizeBytes?.toNumber() || 0;
196
- activator.info.continueAsNewSuggested = activation.continueAsNewSuggested ?? false;
195
+ activator.mutateWorkflowInfo((info) => ({
196
+ ...info,
197
+ historyLength: activation.historyLength as number,
198
+ // Exact truncation for multi-petabyte histories
199
+ // historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
200
+ historySize: activation.historySizeBytes?.toNumber() || 0,
201
+ continueAsNewSuggested: activation.continueAsNewSuggested ?? false,
202
+ currentBuildId: activation.buildIdForCurrentTask ?? undefined,
203
+ unsafe: {
204
+ ...info.unsafe,
205
+ isReplaying: activation.isReplaying ?? false,
206
+ },
207
+ }));
197
208
  }
198
209
 
199
210
  // Cast from the interface to the class which has the `variant` attribute.
@@ -235,9 +246,11 @@ export function activate(activation: coresdk.workflow_activation.WorkflowActivat
235
246
  */
236
247
  export function concludeActivation(): coresdk.workflow_completion.IWorkflowActivationCompletion {
237
248
  const activator = getActivator();
249
+ activator.rejectBufferedUpdates();
238
250
  const intercept = composeInterceptors(activator.interceptors.internals, 'concludeActivation', (input) => input);
239
251
  const { info } = activator;
240
252
  const { commands } = intercept({ commands: activator.getAndResetCommands() });
253
+
241
254
  return {
242
255
  runId: info.runId,
243
256
  successful: { commands },