@temporalio/workflow 1.12.2 → 1.13.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
@@ -41,7 +41,14 @@ import { alea, RNG } from './alea';
41
41
  import { RootCancellationScope } from './cancellation-scope';
42
42
  import { UpdateScope } from './update-scope';
43
43
  import { DeterminismViolationError, LocalActivityDoBackoff, isCancellation } from './errors';
44
- import { QueryInput, SignalInput, UpdateInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
44
+ import {
45
+ QueryInput,
46
+ SignalInput,
47
+ StartNexusOperationOutput,
48
+ UpdateInput,
49
+ WorkflowExecuteInput,
50
+ WorkflowInterceptors,
51
+ } from './interceptors';
45
52
  import {
46
53
  ContinueAsNew,
47
54
  DefaultSignalHandler,
@@ -95,10 +102,9 @@ export interface PromiseStackStore {
95
102
  promiseToStack: Map<Promise<unknown>, Stack>;
96
103
  }
97
104
 
98
- export interface Completion {
99
- resolve(val: unknown): unknown;
100
-
101
- reject(reason: unknown): unknown;
105
+ export interface Completion<Success> {
106
+ resolve(val: Success): void;
107
+ reject(reason: Error): void;
102
108
  }
103
109
 
104
110
  export interface Condition {
@@ -127,6 +133,8 @@ interface MessageHandlerExecution {
127
133
  id?: string;
128
134
  }
129
135
 
136
+ type InferMapValue<T> = T extends Map<number, infer V> ? V : never;
137
+
130
138
  /**
131
139
  * Keeps all of the Workflow runtime state like pending completions for activities and timers.
132
140
  *
@@ -150,16 +158,19 @@ export class Activator implements ActivationHandler {
150
158
  * Cache for modules - referenced in reusable-vm.ts
151
159
  */
152
160
  readonly moduleCache = new Map<string, unknown>();
161
+
153
162
  /**
154
163
  * Map of task sequence to a Completion
155
164
  */
156
165
  readonly completions = {
157
- timer: new Map<number, Completion>(),
158
- activity: new Map<number, Completion>(),
159
- childWorkflowStart: new Map<number, Completion>(),
160
- childWorkflowComplete: new Map<number, Completion>(),
161
- signalWorkflow: new Map<number, Completion>(),
162
- cancelWorkflow: new Map<number, Completion>(),
166
+ timer: new Map<number, Completion<void>>(),
167
+ activity: new Map<number, Completion<unknown>>(),
168
+ nexusOperationStart: new Map<number, Completion<StartNexusOperationOutput>>(),
169
+ nexusOperationComplete: new Map<number, Completion<unknown>>(),
170
+ childWorkflowStart: new Map<number, Completion<string>>(),
171
+ childWorkflowComplete: new Map<number, Completion<unknown>>(),
172
+ signalWorkflow: new Map<number, Completion<void>>(),
173
+ cancelWorkflow: new Map<number, Completion<void>>(),
163
174
  };
164
175
 
165
176
  /**
@@ -381,6 +392,7 @@ export class Activator implements ActivationHandler {
381
392
  signalWorkflow: 1,
382
393
  cancelWorkflow: 1,
383
394
  condition: 1,
395
+ nexusOperation: 1,
384
396
  // Used internally to keep track of active stack traces
385
397
  stack: 1,
386
398
  };
@@ -519,7 +531,7 @@ export class Activator implements ActivationHandler {
519
531
 
520
532
  public async startWorkflowNextHandler({ args }: WorkflowExecuteInput): Promise<any> {
521
533
  const { workflow } = this;
522
- if (workflow === undefined) {
534
+ if (workflow == null) {
523
535
  throw new IllegalStateError('Workflow uninitialized');
524
536
  }
525
537
  return await workflow(...args);
@@ -583,12 +595,16 @@ export class Activator implements ActivationHandler {
583
595
  resolve(result);
584
596
  } else if (activation.result.failed) {
585
597
  const { failure } = activation.result.failed;
586
- const err = failure ? this.failureToError(failure) : undefined;
587
- reject(err);
598
+ if (failure == null) {
599
+ throw new TypeError('Got failed result with no failure attribute');
600
+ }
601
+ reject(this.failureToError(failure));
588
602
  } else if (activation.result.cancelled) {
589
603
  const { failure } = activation.result.cancelled;
590
- const err = failure ? this.failureToError(failure) : undefined;
591
- reject(err);
604
+ if (failure == null) {
605
+ throw new TypeError('Got cancelled result with no failure attribute');
606
+ }
607
+ reject(this.failureToError(failure));
592
608
  } else if (activation.result.backoff) {
593
609
  reject(new LocalActivityDoBackoff(activation.result.backoff));
594
610
  }
@@ -599,6 +615,9 @@ export class Activator implements ActivationHandler {
599
615
  ): void {
600
616
  const { resolve, reject } = this.consumeCompletion('childWorkflowStart', getSeq(activation));
601
617
  if (activation.succeeded) {
618
+ if (!activation.succeeded.runId) {
619
+ throw new TypeError('Got ResolveChildWorkflowExecutionStart with no runId');
620
+ }
602
621
  resolve(activation.succeeded.runId);
603
622
  } else if (activation.failed) {
604
623
  if (decodeStartChildWorkflowExecutionFailedCause(activation.failed.cause) !== 'WORKFLOW_ALREADY_EXISTS') {
@@ -635,25 +654,68 @@ export class Activator implements ActivationHandler {
635
654
  resolve(result);
636
655
  } else if (activation.result.failed) {
637
656
  const { failure } = activation.result.failed;
638
- if (failure === undefined || failure === null) {
657
+ if (failure == null) {
639
658
  throw new TypeError('Got failed result with no failure attribute');
640
659
  }
641
660
  reject(this.failureToError(failure));
642
661
  } else if (activation.result.cancelled) {
643
662
  const { failure } = activation.result.cancelled;
644
- if (failure === undefined || failure === null) {
663
+ if (failure == null) {
645
664
  throw new TypeError('Got cancelled result with no failure attribute');
646
665
  }
647
666
  reject(this.failureToError(failure));
648
667
  }
649
668
  }
650
669
 
651
- public resolveNexusOperationStart(_: coresdk.workflow_activation.IResolveNexusOperationStart): void {
652
- throw new Error('TODO');
670
+ public resolveNexusOperationStart(activation: coresdk.workflow_activation.IResolveNexusOperationStart): void {
671
+ const seq = getSeq(activation);
672
+ const { resolve, reject } = this.consumeCompletion('nexusOperationStart', seq);
673
+
674
+ if (!activation.failed) {
675
+ const completePromise = new Promise((resolve, reject) => {
676
+ this.completions.nexusOperationComplete.set(seq, {
677
+ resolve,
678
+ reject,
679
+ });
680
+ });
681
+ untrackPromise(completePromise);
682
+ untrackPromise(completePromise.catch(() => undefined));
683
+
684
+ resolve({ token: activation.operationToken!, result: completePromise });
685
+ } else {
686
+ reject(this.failureToError(activation.failed));
687
+ }
653
688
  }
654
689
 
655
- public resolveNexusOperation(_: coresdk.workflow_activation.IResolveNexusOperation): void {
656
- throw new Error('TODO');
690
+ public resolveNexusOperation(activation: coresdk.workflow_activation.IResolveNexusOperation): void {
691
+ const seq = getSeq(activation);
692
+
693
+ if (activation.result?.completed) {
694
+ const result = this.payloadConverter.fromPayload(activation.result.completed);
695
+
696
+ // It is possible for ResolveNexusOperation to be received without a prior ResolveNexusOperationStart,
697
+ // e.g. because the handler completed the Operation synchronously.
698
+ const startCompletion = this.maybeConsumeCompletion('nexusOperationStart', seq);
699
+ if (startCompletion) {
700
+ startCompletion.resolve({ result: Promise.resolve(result) });
701
+ } else {
702
+ this.consumeCompletion('nexusOperationComplete', seq).resolve(result);
703
+ }
704
+ } else {
705
+ let err: Error;
706
+ if (activation.result?.failed) {
707
+ err = this.failureToError(activation.result.failed);
708
+ } else if (activation.result?.cancelled) {
709
+ err = this.failureToError(activation.result.cancelled);
710
+ } else if (activation.result?.timedOut) {
711
+ err = this.failureToError(activation.result.timedOut);
712
+ }
713
+
714
+ const completion =
715
+ this.maybeConsumeCompletion('nexusOperationStart', seq) ??
716
+ this.consumeCompletion('nexusOperationComplete', seq);
717
+ completion.reject(err!);
718
+ }
657
719
  }
658
720
 
659
721
  // Intentionally non-async function so this handler doesn't show up in the stack trace
@@ -749,7 +811,7 @@ export class Activator implements ActivationHandler {
749
811
  : null);
750
812
 
751
813
  // If we don't have an entry from either source, buffer and return
752
- if (entry === null) {
814
+ if (entry == null) {
753
815
  this.bufferedUpdates.push(activation);
754
816
  return;
755
817
  }
@@ -1152,16 +1214,22 @@ export class Activator implements ActivationHandler {
1152
1214
  }
1153
1215
 
1154
1216
  /** Consume a completion if it exists in Workflow state */
1155
- private maybeConsumeCompletion(type: keyof Activator['completions'], taskSeq: number): Completion | undefined {
1217
+ private maybeConsumeCompletion<K extends keyof Activator['completions']>(
1218
+ type: K,
1219
+ taskSeq: number
1220
+ ): InferMapValue<Activator['completions'][K]> | undefined {
1156
1221
  const completion = this.completions[type].get(taskSeq);
1157
1222
  if (completion !== undefined) {
1158
1223
  this.completions[type].delete(taskSeq);
1159
1224
  }
1160
- return completion;
1225
+ return completion as InferMapValue<Activator['completions'][K]> | undefined;
1161
1226
  }
1162
1227
 
1163
1228
  /** Consume a completion if it exists in Workflow state, throws if it doesn't */
1164
- private consumeCompletion(type: keyof Activator['completions'], taskSeq: number): Completion {
1229
+ private consumeCompletion<K extends keyof Activator['completions']>(
1230
+ type: K,
1231
+ taskSeq: number
1232
+ ): InferMapValue<Activator['completions'][K]> {
1165
1233
  const completion = this.maybeConsumeCompletion(type, taskSeq);
1166
1234
  if (completion === undefined) {
1167
1235
  throw new IllegalStateError(`No completion for taskSeq ${taskSeq}`);
@@ -1191,7 +1259,7 @@ export class Activator implements ActivationHandler {
1191
1259
 
1192
1260
  function getSeq<T extends { seq?: number | null }>(activation: T): number {
1193
1261
  const seq = activation.seq;
1194
- if (seq === undefined || seq === null) {
1262
+ if (seq == null) {
1195
1263
  throw new TypeError(`Got activation with no seq attribute`);
1196
1264
  }
1197
1265
  return seq;
package/src/nexus.ts ADDED
@@ -0,0 +1,228 @@
1
+ import * as nexus from 'nexus-rpc';
2
+ import { msOptionalToTs } from '@temporalio/common/lib/time';
3
+ import { userMetadataToPayload } from '@temporalio/common/lib/user-metadata';
4
+ import { composeInterceptors } from '@temporalio/common/lib/interceptors';
5
+ import { CancellationScope } from './cancellation-scope';
6
+ import { getActivator } from './global-attributes';
7
+ import { untrackPromise } from './stack-helpers';
8
+ import { StartNexusOperationInput, StartNexusOperationOutput, StartNexusOperationOptions } from './interceptors';
9
+
10
+ /**
11
+ * A Nexus client for invoking Nexus Operations for a specific service from a Workflow.
12
+ *
13
+ * @experimental Nexus support in Temporal SDK is experimental.
14
+ */
15
+ export interface NexusClient<T extends nexus.ServiceDefinition> {
16
+ /**
17
+ * Start a Nexus Operation and wait for its completion taking a {@link nexus.operation}.
18
+ * Returns the operation's result.
19
+ *
20
+ * @experimental Nexus support in Temporal SDK is experimental.
21
+ */
22
+ executeOperation<O extends T['operations'][keyof T['operations']]>(
23
+ op: O,
24
+ input: nexus.OperationInput<O>,
25
+ options?: Partial<StartNexusOperationOptions>
26
+ ): Promise<nexus.OperationOutput<O>>;
27
+
28
+ // TODO(nexus/post-initial-release): Revisit the "Operation Property Name" terminology,
29
+ // and reflect in the Nexus RPC SDK.
30
+
31
+ /**
32
+ * Start a Nexus Operation and wait for its completion, taking an Operation's _property name_.
33
+ * Returns the operation's result.
34
+ *
35
+ * An Operation's _property name_ is the name of the property used to define that Operation in
36
+ * the {@link nexus.ServiceDefinition} object; it may differ from the value of the `name` property
37
+ * if one was explicitly specified on the {@link nexus.OperationDefinition} object.
38
+ *
39
+ * @experimental Nexus support in Temporal SDK is experimental.
40
+ */
41
+ executeOperation<K extends nexus.OperationKey<T['operations']>>(
42
+ op: K,
43
+ input: nexus.OperationInput<T['operations'][K]>,
44
+ options?: Partial<StartNexusOperationOptions>
45
+ ): Promise<nexus.OperationOutput<T['operations'][K]>>;
46
+
47
+ /**
48
+ * Start a Nexus Operation taking a {@link nexus.operation}.
49
+ *
50
+ * Returns a handle that can be used to wait for the Operation's result.
51
+ *
52
+ * @experimental Nexus support in Temporal SDK is experimental.
53
+ */
54
+ startOperation<O extends T['operations'][keyof T['operations']]>(
55
+ op: O,
56
+ input: nexus.OperationInput<O>,
57
+ options?: Partial<StartNexusOperationOptions>
58
+ ): Promise<NexusOperationHandle<nexus.OperationOutput<O>>>;
59
+
60
+ /**
61
+ * Start a Nexus Operation, taking an Operation's _property name_.
62
+ * Returns a handle that can be used to wait for the Operation's result.
63
+ *
64
+ * An Operation's _property name_ is the name of the property used to define that Operation in
65
+ * the {@link nexus.ServiceDefinition} object; it may differ from the value of the `name` property
66
+ * if one was explicitly specified on the {@link nexus.OperationDefinition} object.
67
+ *
68
+ * @experimental Nexus support in Temporal SDK is experimental.
69
+ */
70
+ startOperation<K extends nexus.OperationKey<T['operations']>>(
71
+ op: K,
72
+ input: nexus.OperationInput<T['operations'][K]>,
73
+ options?: Partial<StartNexusOperationOptions>
74
+ ): Promise<NexusOperationHandle<nexus.OperationOutput<T['operations'][K]>>>;
75
+ }
76
+
77
+ /**
78
+ * A handle to a Nexus Operation.
79
+ *
80
+ * @experimental Nexus support in Temporal SDK is experimental.
81
+ */
82
+ export interface NexusOperationHandle<T> {
83
+ /**
84
+ * The Operation's service name.
85
+ */
86
+ readonly service: string;
87
+
88
+ /**
89
+ * The name of the Operation.
90
+ */
91
+ readonly operation: string;
92
+
93
+ /**
94
+ * Operation token as set by the Operation's handler. May be empty if the Operation completed synchronously.
95
+ */
96
+ readonly token?: string;
97
+
98
+ /**
99
+ * Wait for Operation completion and get its result.
100
+ */
101
+ result(): Promise<T>;
102
+ }
103
+
104
+ /**
105
+ * Options for {@link createNexusClient}.
106
+ */
107
+ export interface NexusClientOptions<T> {
108
+ endpoint: string;
109
+ service: T;
110
+ }
111
+
112
+ /**
113
+ * Create a Nexus client for invoking Nexus Operations from a Workflow.
114
+ *
115
+ * @experimental Nexus support in Temporal SDK is experimental.
116
+ */
117
+ export function createNexusClient<T extends nexus.ServiceDefinition>(options: NexusClientOptions<T>): NexusClient<T> {
118
+ class NexusClientImpl<T extends nexus.ServiceDefinition> implements NexusClient<T> {
119
+ async executeOperation<O extends T['operations'][keyof T['operations']]>(
120
+ operation: string | T['operations'][nexus.OperationKey<T['operations']>],
121
+ input: nexus.OperationInput<T['operations'][nexus.OperationKey<T['operations']>]>,
122
+ operationOptions?: Partial<StartNexusOperationOptions>
123
+ ): Promise<nexus.OperationOutput<O>> {
124
+ const handle = await this.startOperation(operation, input, operationOptions);
125
+ return await handle.result();
126
+ }
127
+
128
+ async startOperation<O extends T['operations'][keyof T['operations']]>(
129
+ operation: string | T['operations'][nexus.OperationKey<T['operations']>],
130
+ input: nexus.OperationInput<T['operations'][nexus.OperationKey<T['operations']>]>,
131
+ operationOptions?: StartNexusOperationOptions
132
+ ) {
133
+ const opName = typeof operation === 'string' ? options.service.operations[operation]?.name : operation.name;
134
+
135
+ const activator = getActivator();
136
+ const seq = activator.nextSeqs.nexusOperation++;
137
+
138
+ const execute = composeInterceptors(
139
+ activator.interceptors.outbound,
140
+ 'startNexusOperation',
141
+ startNexusOperationNextHandler
142
+ );
143
+
144
+ // TODO: Do we want to make the interceptor async like we do for child workflow? That seems redundant.
145
+ // REVIEW: I ended up changing this so that the interceptor returns a Promise<StartNexusOperationOutput>,
146
+ // and the result promise is contained in that Output object. As a consequence of this,
147
+ // the result promise/completion does not exist until the StartNexusOperation event is received.
148
+ // That's totally different from what we did in ChildWorkflow, but I think that's cleaner from
149
+ // interceptors point of view, and will make it easier to extend the API in the future.
150
+ const { token, result: resultPromise } = await execute({
151
+ endpoint: options.endpoint,
152
+ service: options.service.name,
153
+ operation: opName,
154
+ options: operationOptions ?? {},
155
+ headers: {},
156
+ seq,
157
+ input,
158
+ });
159
+
160
+ return {
161
+ service: options.service.name,
162
+ operation: opName,
163
+ token,
164
+ async result(): Promise<nexus.OperationOutput<O>> {
165
+ return resultPromise as nexus.OperationOutput<O>;
166
+ },
167
+ };
168
+ }
169
+ }
170
+
171
+ return new NexusClientImpl<T>();
172
+ }
173
+
174
+ function startNexusOperationNextHandler({
175
+ input,
176
+ endpoint,
177
+ service,
178
+ options,
179
+ operation,
180
+ seq,
181
+ headers,
182
+ }: StartNexusOperationInput): Promise<StartNexusOperationOutput> {
183
+ const activator = getActivator();
184
+
185
+ return new Promise<StartNexusOperationOutput>((resolve, reject) => {
186
+ const scope = CancellationScope.current();
187
+ if (scope.consideredCancelled) {
188
+ untrackPromise(scope.cancelRequested.catch(reject));
189
+ return;
190
+ }
191
+ if (scope.cancellable) {
192
+ untrackPromise(
193
+ scope.cancelRequested.catch(() => {
194
+ const completed =
195
+ !activator.completions.nexusOperationStart.has(seq) &&
196
+ !activator.completions.nexusOperationComplete.has(seq);
197
+
198
+ if (!completed) {
199
+ activator.pushCommand({
200
+ requestCancelNexusOperation: { seq },
201
+ });
202
+ }
203
+
204
+ // Nothing to cancel otherwise
205
+ })
206
+ );
207
+ }
208
+
209
+ activator.pushCommand({
210
+ scheduleNexusOperation: {
211
+ seq,
212
+ endpoint,
213
+ service,
214
+ operation,
215
+ nexusHeader: headers,
216
+ input: activator.payloadConverter.toPayload(input),
217
+ scheduleToCloseTimeout: msOptionalToTs(options?.scheduleToCloseTimeout),
218
+ // FIXME(nexus-post-initial-release): cancellationType is not supported yet
219
+ },
220
+ userMetadata: userMetadataToPayload(activator.payloadConverter, options?.summary, undefined),
221
+ });
222
+
223
+ activator.completions.nexusOperationStart.set(seq, {
224
+ resolve,
225
+ reject,
226
+ });
227
+ });
228
+ }
package/src/workflow.ts CHANGED
@@ -523,8 +523,7 @@ export type ActivityFunctionWithOptions<T extends ActivityFunction> = T & {
523
523
  * @param args: list of arguments
524
524
  * @returns return value of the activity
525
525
  *
526
- * @experimental executeWithOptions is a new method to provide call-site options
527
- * and is subject to change
526
+ * @experimental executeWithOptions is a new method to provide call-site options and is subject to change
528
527
  */
529
528
  executeWithOptions(options: ActivityOptions, args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
530
529
  };
@@ -545,8 +544,7 @@ export type LocalActivityFunctionWithOptions<T extends ActivityFunction> = T & {
545
544
  * @param args: list of arguments
546
545
  * @returns return value of the activity
547
546
  *
548
- * @experimental executeWithOptions is a new method to provide call-site options
549
- * and is subject to change
547
+ * @experimental executeWithOptions is a new method to provide call-site options and is subject to change
550
548
  */
551
549
  executeWithOptions(options: LocalActivityOptions, args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
552
550
  };