@temporalio/client 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.
@@ -10,6 +10,7 @@ import {
10
10
  RetryState,
11
11
  searchAttributePayloadConverter,
12
12
  SignalDefinition,
13
+ UpdateDefinition,
13
14
  TerminatedFailure,
14
15
  TimeoutFailure,
15
16
  TimeoutType,
@@ -32,7 +33,13 @@ import {
32
33
  filterNullAndUndefined,
33
34
  } from '@temporalio/common/lib/internal-non-workflow';
34
35
  import { temporal } from '@temporalio/proto';
35
- import { ServiceError, WorkflowContinuedAsNewError, WorkflowFailedError, isGrpcServiceError } from './errors';
36
+ import {
37
+ ServiceError,
38
+ WorkflowContinuedAsNewError,
39
+ WorkflowFailedError,
40
+ WorkflowUpdateFailedError,
41
+ isGrpcServiceError,
42
+ } from './errors';
36
43
  import {
37
44
  WorkflowCancelInput,
38
45
  WorkflowClientInterceptor,
@@ -43,6 +50,8 @@ import {
43
50
  WorkflowSignalWithStartInput,
44
51
  WorkflowStartInput,
45
52
  WorkflowTerminateInput,
53
+ WorkflowStartUpdateInput,
54
+ WorkflowStartUpdateOutput,
46
55
  } from './interceptors';
47
56
  import {
48
57
  DescribeWorkflowExecutionResponse,
@@ -60,6 +69,7 @@ import {
60
69
  WorkflowOptions,
61
70
  WorkflowSignalWithStartOptions,
62
71
  WorkflowStartOptions,
72
+ WorkflowUpdateOptions,
63
73
  } from './workflow-options';
64
74
  import { executionInfoFromRaw, rethrowKnownErrorTypes } from './helpers';
65
75
  import {
@@ -77,8 +87,9 @@ import { mapAsyncIterable } from './iterators-utils';
77
87
  *
78
88
  * Given the following Workflow definition:
79
89
  * ```ts
80
- * export const incrementSignal = defineSignal('increment');
90
+ * export const incrementSignal = defineSignal<[number]>('increment');
81
91
  * export const getValueQuery = defineQuery<number>('getValue');
92
+ * export const incrementAndGetValueUpdate = defineUpdate<number, [number]>('incrementAndGetValue');
82
93
  * export async function counterWorkflow(initialValue: number): Promise<void>;
83
94
  * ```
84
95
  *
@@ -92,12 +103,72 @@ import { mapAsyncIterable } from './iterators-utils';
92
103
  * taskQueue: 'tutorial',
93
104
  * });
94
105
  * await handle.signal(incrementSignal, 2);
95
- * await handle.query(getValueQuery); // 4
106
+ * const queryResult = await handle.query(getValueQuery); // 4
107
+ * const firstUpdateResult = await handle.executeUpdate(incrementAndGetValueUpdate, { args: [2] }); // 6
108
+ * const secondUpdateHandle = await handle.startUpdate(incrementAndGetValueUpdate, { args: [2] });
109
+ * const secondUpdateResult = await secondUpdateHandle.result(); // 8
96
110
  * await handle.cancel();
97
- * await handle.result(); // throws WorkflowExecutionCancelledError
111
+ * await handle.result(); // throws a WorkflowFailedError with `cause` set to a CancelledFailure.
98
112
  * ```
99
113
  */
100
114
  export interface WorkflowHandle<T extends Workflow = Workflow> extends BaseWorkflowHandle<T> {
115
+ /**
116
+ * Start an Update and wait for the result.
117
+ *
118
+ * @experimental Update is an experimental feature.
119
+ *
120
+ * @throws {@link WorkflowUpdateFailedError} if Update validation fails or if ApplicationFailure is thrown in the Update handler.
121
+ *
122
+ * @param def an Update definition as returned from {@link defineUpdate}
123
+ * @param options Update arguments
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const updateResult = await handle.executeUpdate(incrementAndGetValueUpdate, { args: [2] });
128
+ * ```
129
+ */
130
+ executeUpdate<Ret, Args extends [any, ...any[]], Name extends string = string>(
131
+ def: UpdateDefinition<Ret, Args, Name> | string,
132
+ options: WorkflowUpdateOptions & { args: Args }
133
+ ): Promise<Ret>;
134
+
135
+ executeUpdate<Ret, Args extends [], Name extends string = string>(
136
+ def: UpdateDefinition<Ret, Args, Name> | string,
137
+ options?: WorkflowUpdateOptions & { args?: Args }
138
+ ): Promise<Ret>;
139
+
140
+ /**
141
+ * Start an Update and receive a handle to the Update.
142
+ * The Update validator (if present) is run before the handle is returned.
143
+ *
144
+ * @experimental Update is an experimental feature.
145
+ *
146
+ * @throws {@link WorkflowUpdateFailedError} if Update validation fails.
147
+ *
148
+ * @param def an Update definition as returned from {@link defineUpdate}
149
+ * @param options Update arguments
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const updateHandle = await handle.startUpdate(incrementAndGetValueUpdate, { args: [2] });
154
+ * const updateResult = await updateHandle.result();
155
+ * ```
156
+ */
157
+ startUpdate<Ret, Args extends [any, ...any[]], Name extends string = string>(
158
+ def: UpdateDefinition<Ret, Args, Name> | string,
159
+ options: WorkflowUpdateOptions & { args: Args }
160
+ ): Promise<WorkflowUpdateHandle<Ret>>;
161
+
162
+ startUpdate<Ret, Args extends [], Name extends string = string>(
163
+ def: UpdateDefinition<Ret, Args, Name> | string,
164
+ options?: WorkflowUpdateOptions & { args?: Args }
165
+ ): Promise<WorkflowUpdateHandle<Ret>>;
166
+
167
+ /**
168
+ * Get a handle to an Update of this Workflow.
169
+ */
170
+ getUpdateHandle<Ret>(updateId: string): WorkflowUpdateHandle<Ret>;
171
+
101
172
  /**
102
173
  * Query a running or completed Workflow.
103
174
  *
@@ -230,6 +301,9 @@ export interface WorkflowResultOptions {
230
301
  followRuns?: boolean;
231
302
  }
232
303
 
304
+ /**
305
+ * Options for {@link WorkflowClient.getHandle}
306
+ */
233
307
  export interface GetWorkflowHandleOptions extends WorkflowResultOptions {
234
308
  /**
235
309
  * ID of the first execution in the Workflow execution chain.
@@ -268,6 +342,42 @@ export interface AsyncWorkflowListIterable extends AsyncIterable<WorkflowExecuti
268
342
  intoHistories: (intoHistoriesOptions?: IntoHistoriesOptions) => AsyncIterable<HistoryAndWorkflowId>;
269
343
  }
270
344
 
345
+ /**
346
+ * A client-side handle to an Update.
347
+ */
348
+ export interface WorkflowUpdateHandle<Ret> {
349
+ /**
350
+ * The ID of this Update request.
351
+ */
352
+ updateId: string;
353
+
354
+ /**
355
+ * The ID of the Workflow being targeted by this Update request.
356
+ */
357
+ workflowId: string;
358
+
359
+ /**
360
+ * The ID of the Run of the Workflow being targeted by this Update request.
361
+ */
362
+ workflowRunId?: string;
363
+
364
+ /**
365
+ * Return the result of the Update.
366
+ * @throws {@link WorkflowUpdateFailedError} if ApplicationFailure is thrown in the Update handler.
367
+ */
368
+ result(): Promise<Ret>;
369
+ }
370
+
371
+ /**
372
+ * Options for {@link WorkflowHandle.getUpdateHandle}
373
+ */
374
+ export interface GetWorkflowUpdateHandleOptions {
375
+ /**
376
+ * The ID of the Run of the Workflow targeted by the Update.
377
+ */
378
+ workflowRunId?: string;
379
+ }
380
+
271
381
  /**
272
382
  * Options for {@link WorkflowClient.list}
273
383
  */
@@ -362,7 +472,7 @@ export class WorkflowClient extends BaseClient {
362
472
 
363
473
  /**
364
474
  * Sends a signal to a running Workflow or starts a new one if not already running and immediately signals it.
365
- * Useful when you're unsure of the Workflows' run state.
475
+ * Useful when you're unsure of the Workflow's run state.
366
476
  *
367
477
  * @returns the runId of the Workflow
368
478
  */
@@ -640,6 +750,115 @@ export class WorkflowClient extends BaseClient {
640
750
  return await decodeFromPayloadsAtIndex(this.dataConverter, 0, response.queryResult?.payloads);
641
751
  }
642
752
 
753
+ /**
754
+ * Start the Update.
755
+ *
756
+ * Used as the final function of the interceptor chain during startUpdate and executeUpdate.
757
+ */
758
+ protected async _startUpdateHandler(
759
+ waitForStage: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage,
760
+ input: WorkflowStartUpdateInput
761
+ ): Promise<WorkflowStartUpdateOutput> {
762
+ const updateId = input.options?.updateId ?? uuid4();
763
+ const req: temporal.api.workflowservice.v1.IUpdateWorkflowExecutionRequest = {
764
+ namespace: this.options.namespace,
765
+ workflowExecution: input.workflowExecution,
766
+ firstExecutionRunId: input.firstExecutionRunId,
767
+ waitPolicy: { lifecycleStage: waitForStage },
768
+ request: {
769
+ meta: {
770
+ updateId,
771
+ identity: this.options.identity,
772
+ },
773
+ input: {
774
+ header: { fields: input.headers },
775
+ name: input.updateName,
776
+ args: { payloads: await encodeToPayloads(this.dataConverter, ...input.args) },
777
+ },
778
+ },
779
+ };
780
+ let response: temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse;
781
+
782
+ try {
783
+ response = await this.workflowService.updateWorkflowExecution(req);
784
+ } catch (err) {
785
+ this.rethrowGrpcError(err, 'Workflow Update failed', input.workflowExecution);
786
+ }
787
+ return {
788
+ updateId,
789
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
790
+ workflowRunId: response.updateRef!.workflowExecution!.runId!,
791
+ outcome: response.outcome ?? undefined,
792
+ };
793
+ }
794
+
795
+ protected createWorkflowUpdateHandle<Ret>(
796
+ updateId: string,
797
+ workflowId: string,
798
+ workflowRunId?: string,
799
+ outcome?: temporal.api.update.v1.IOutcome
800
+ ): WorkflowUpdateHandle<Ret> {
801
+ return {
802
+ updateId,
803
+ workflowId,
804
+ workflowRunId,
805
+ result: async () => {
806
+ const completedOutcome =
807
+ outcome ?? (await this._pollForUpdateOutcome(updateId, { workflowId, runId: workflowRunId }));
808
+ if (completedOutcome.failure) {
809
+ throw new WorkflowUpdateFailedError(
810
+ 'Workflow Update failed',
811
+ await decodeOptionalFailureToOptionalError(this.dataConverter, completedOutcome.failure)
812
+ );
813
+ } else {
814
+ return await decodeFromPayloadsAtIndex<Ret>(this.dataConverter, 0, completedOutcome.success?.payloads);
815
+ }
816
+ },
817
+ };
818
+ }
819
+
820
+ /**
821
+ * Poll Update until a response with an outcome is received; return that outcome.
822
+ * This is used directly; no interceptor is available.
823
+ */
824
+ protected async _pollForUpdateOutcome(
825
+ updateId: string,
826
+ workflowExecution: temporal.api.common.v1.IWorkflowExecution
827
+ ): Promise<temporal.api.update.v1.IOutcome> {
828
+ const req: temporal.api.workflowservice.v1.IPollWorkflowExecutionUpdateRequest = {
829
+ namespace: this.options.namespace,
830
+ updateRef: { workflowExecution, updateId },
831
+ identity: this.options.identity,
832
+ waitPolicy: {
833
+ lifecycleStage:
834
+ temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage
835
+ .UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
836
+ },
837
+ };
838
+
839
+ // TODO: Users should be able to use client.withDeadline(timestamp) with a
840
+ // Date (as opposed to a duration) to control the total amount of time
841
+ // allowed for polling. However, this requires a server change such that the
842
+ // server swallows the gRPC timeout and instead responds with a well-formed
843
+ // PollWorkflowExecutionUpdateResponse, indicating that the requested
844
+ // lifecycle stage has not yet been reached at the time of the deadline
845
+ // expiry. See https://github.com/temporalio/temporal/issues/4742
846
+
847
+ // TODO: When temporal#4742 is released, stop catching DEADLINE_EXCEEDED.
848
+ for (;;) {
849
+ try {
850
+ const response = await this.workflowService.pollWorkflowExecutionUpdate(req);
851
+ if (response.outcome) {
852
+ return response.outcome;
853
+ }
854
+ } catch (err) {
855
+ if (!(isGrpcServiceError(err) && err.code === grpcStatus.DEADLINE_EXCEEDED)) {
856
+ throw err;
857
+ }
858
+ }
859
+ }
860
+ }
861
+
643
862
  /**
644
863
  * Uses given input to make a signalWorkflowExecution call to the service
645
864
  *
@@ -688,6 +907,7 @@ export class WorkflowClient extends BaseClient {
688
907
  workflowExecutionTimeout: options.workflowExecutionTimeout,
689
908
  workflowRunTimeout: options.workflowRunTimeout,
690
909
  workflowTaskTimeout: options.workflowTaskTimeout,
910
+ workflowStartDelay: options.startDelay,
691
911
  retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
692
912
  memo: options.memo ? { fields: await encodeMapToPayloads(this.dataConverter, options.memo) } : undefined,
693
913
  searchAttributes: options.searchAttributes
@@ -735,6 +955,7 @@ export class WorkflowClient extends BaseClient {
735
955
  workflowExecutionTimeout: opts.workflowExecutionTimeout,
736
956
  workflowRunTimeout: opts.workflowRunTimeout,
737
957
  workflowTaskTimeout: opts.workflowTaskTimeout,
958
+ workflowStartDelay: opts.startDelay,
738
959
  retryPolicy: opts.retry ? compileRetryPolicy(opts.retry) : undefined,
739
960
  memo: opts.memo ? { fields: await encodeMapToPayloads(this.dataConverter, opts.memo) } : undefined,
740
961
  searchAttributes: opts.searchAttributes
@@ -829,6 +1050,33 @@ export class WorkflowClient extends BaseClient {
829
1050
  runIdForResult,
830
1051
  ...resultOptions
831
1052
  }: WorkflowHandleOptions): WorkflowHandle<T> {
1053
+ // TODO (dan): Convert to class with this as a protected method
1054
+ const _startUpdate = async <Ret, Args extends unknown[]>(
1055
+ def: UpdateDefinition<Ret, Args> | string,
1056
+ waitForStage: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage,
1057
+ options?: WorkflowUpdateOptions & { args?: Args }
1058
+ ): Promise<WorkflowUpdateHandle<Ret>> => {
1059
+ const next = this._startUpdateHandler.bind(this, waitForStage);
1060
+ const fn = composeInterceptors(interceptors, 'startUpdate', next);
1061
+ const { args, ...opts } = options ?? {};
1062
+ const input = {
1063
+ workflowExecution: { workflowId, runId },
1064
+ firstExecutionRunId,
1065
+ updateName: typeof def === 'string' ? def : def.name,
1066
+ args: args ?? [],
1067
+ waitForStage,
1068
+ headers: {},
1069
+ options: opts,
1070
+ };
1071
+ const output = await fn(input);
1072
+ return this.createWorkflowUpdateHandle<Ret>(
1073
+ output.updateId,
1074
+ input.workflowExecution.workflowId,
1075
+ output.workflowRunId,
1076
+ output.outcome
1077
+ );
1078
+ };
1079
+
832
1080
  return {
833
1081
  client: this,
834
1082
  workflowId,
@@ -837,7 +1085,7 @@ export class WorkflowClient extends BaseClient {
837
1085
  },
838
1086
  async terminate(reason?: string) {
839
1087
  const next = this.client._terminateWorkflowHandler.bind(this.client);
840
- const fn = interceptors.length ? composeInterceptors(interceptors, 'terminate', next) : next;
1088
+ const fn = composeInterceptors(interceptors, 'terminate', next);
841
1089
  return await fn({
842
1090
  workflowExecution: { workflowId, runId },
843
1091
  reason,
@@ -846,7 +1094,7 @@ export class WorkflowClient extends BaseClient {
846
1094
  },
847
1095
  async cancel() {
848
1096
  const next = this.client._cancelWorkflowHandler.bind(this.client);
849
- const fn = interceptors.length ? composeInterceptors(interceptors, 'cancel', next) : next;
1097
+ const fn = composeInterceptors(interceptors, 'cancel', next);
850
1098
  return await fn({
851
1099
  workflowExecution: { workflowId, runId },
852
1100
  firstExecutionRunId,
@@ -854,7 +1102,7 @@ export class WorkflowClient extends BaseClient {
854
1102
  },
855
1103
  async describe() {
856
1104
  const next = this.client._describeWorkflowHandler.bind(this.client);
857
- const fn = interceptors.length ? composeInterceptors(interceptors, 'describe', next) : next;
1105
+ const fn = composeInterceptors(interceptors, 'describe', next);
858
1106
  const raw = await fn({
859
1107
  workflowExecution: { workflowId, runId },
860
1108
  });
@@ -880,9 +1128,35 @@ export class WorkflowClient extends BaseClient {
880
1128
  }
881
1129
  return temporal.api.history.v1.History.create({ events });
882
1130
  },
1131
+ async startUpdate<Ret, Args extends any[]>(
1132
+ def: UpdateDefinition<Ret, Args> | string,
1133
+ options?: WorkflowUpdateOptions & { args?: Args }
1134
+ ): Promise<WorkflowUpdateHandle<Ret>> {
1135
+ return await _startUpdate(
1136
+ def,
1137
+ temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage
1138
+ .UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
1139
+ options
1140
+ );
1141
+ },
1142
+ async executeUpdate<Ret, Args extends any[]>(
1143
+ def: UpdateDefinition<Ret, Args> | string,
1144
+ options?: WorkflowUpdateOptions & { args?: Args }
1145
+ ): Promise<Ret> {
1146
+ const handle = await _startUpdate(
1147
+ def,
1148
+ temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage
1149
+ .UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
1150
+ options
1151
+ );
1152
+ return await handle.result();
1153
+ },
1154
+ getUpdateHandle<Ret>(updateId: string): WorkflowUpdateHandle<Ret> {
1155
+ return this.client.createWorkflowUpdateHandle(updateId, workflowId, runId);
1156
+ },
883
1157
  async signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
884
1158
  const next = this.client._signalWorkflowHandler.bind(this.client);
885
- const fn = interceptors.length ? composeInterceptors(interceptors, 'signal', next) : next;
1159
+ const fn = composeInterceptors(interceptors, 'signal', next);
886
1160
  await fn({
887
1161
  workflowExecution: { workflowId, runId },
888
1162
  signalName: typeof def === 'string' ? def : def.name,
@@ -892,7 +1166,7 @@ export class WorkflowClient extends BaseClient {
892
1166
  },
893
1167
  async query<Ret, Args extends any[]>(def: QueryDefinition<Ret, Args> | string, ...args: Args): Promise<Ret> {
894
1168
  const next = this.client._queryWorkflowHandler.bind(this.client);
895
- const fn = interceptors.length ? composeInterceptors(interceptors, 'query', next) : next;
1169
+ const fn = composeInterceptors(interceptors, 'query', next);
896
1170
  return fn({
897
1171
  workflowExecution: { workflowId, runId },
898
1172
  queryRejectCondition: this.client.options.queryRejectCondition,
@@ -1009,7 +1283,10 @@ export class QueryRejectedError extends Error {
1009
1283
 
1010
1284
  @SymbolBasedInstanceOfError('QueryNotRegisteredError')
1011
1285
  export class QueryNotRegisteredError extends Error {
1012
- constructor(message: string, public readonly code: grpcStatus) {
1286
+ constructor(
1287
+ message: string,
1288
+ public readonly code: grpcStatus
1289
+ ) {
1013
1290
  super(message);
1014
1291
  }
1015
1292
  }
@@ -1,10 +1,7 @@
1
- import {
2
- CommonWorkflowOptions,
3
- SignalDefinition,
4
- WithCompiledWorkflowOptions,
5
- WithWorkflowArgs,
6
- Workflow,
7
- } from '@temporalio/common';
1
+ import { CommonWorkflowOptions, SignalDefinition, WithWorkflowArgs, Workflow } from '@temporalio/common';
2
+ import { Duration, msOptionalToTs } from '@temporalio/common/lib/time';
3
+ import { Replace } from '@temporalio/common/lib/type-helpers';
4
+ import { google } from '@temporalio/proto';
8
5
 
9
6
  export * from '@temporalio/common/lib/workflow-options';
10
7
 
@@ -37,6 +34,39 @@ export interface WorkflowOptions extends CommonWorkflowOptions {
37
34
  * @default true
38
35
  */
39
36
  followRuns?: boolean;
37
+
38
+ /**
39
+ * Amount of time to wait before starting the workflow.
40
+ *
41
+ * @experimental
42
+ */
43
+ startDelay?: Duration;
44
+ }
45
+
46
+ export type WithCompiledWorkflowOptions<T extends WorkflowOptions> = Replace<
47
+ T,
48
+ {
49
+ workflowExecutionTimeout?: google.protobuf.IDuration;
50
+ workflowRunTimeout?: google.protobuf.IDuration;
51
+ workflowTaskTimeout?: google.protobuf.IDuration;
52
+ startDelay?: google.protobuf.IDuration;
53
+ }
54
+ >;
55
+
56
+ export function compileWorkflowOptions<T extends WorkflowOptions>(options: T): WithCompiledWorkflowOptions<T> {
57
+ const { workflowExecutionTimeout, workflowRunTimeout, workflowTaskTimeout, startDelay, ...rest } = options;
58
+
59
+ return {
60
+ ...rest,
61
+ workflowExecutionTimeout: msOptionalToTs(workflowExecutionTimeout),
62
+ workflowRunTimeout: msOptionalToTs(workflowRunTimeout),
63
+ workflowTaskTimeout: msOptionalToTs(workflowTaskTimeout),
64
+ startDelay: msOptionalToTs(startDelay),
65
+ };
66
+ }
67
+
68
+ export interface WorkflowUpdateOptions {
69
+ readonly updateId?: string;
40
70
  }
41
71
 
42
72
  export type WorkflowSignalWithStartOptions<SignalArgs extends any[] = []> = SignalArgs extends [any, ...any[]]