@temporalio/client 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.
@@ -38,6 +38,7 @@ import {
38
38
  WorkflowContinuedAsNewError,
39
39
  WorkflowFailedError,
40
40
  WorkflowUpdateFailedError,
41
+ WorkflowUpdateRPCTimeoutOrCancelledError,
41
42
  isGrpcServiceError,
42
43
  } from './errors';
43
44
  import {
@@ -80,6 +81,8 @@ import {
80
81
  WithDefaults,
81
82
  } from './base-client';
82
83
  import { mapAsyncIterable } from './iterators-utils';
84
+ import { WorkflowUpdateStage } from './workflow-update-stage';
85
+ import * as workflowUpdateStage from './workflow-update-stage';
83
86
 
84
87
  /**
85
88
  * A client side handle to a single Workflow instance.
@@ -118,7 +121,8 @@ export interface WorkflowHandle<T extends Workflow = Workflow> extends BaseWorkf
118
121
  * @experimental Update is an experimental feature.
119
122
  *
120
123
  * @throws {@link WorkflowUpdateFailedError} if Update validation fails or if ApplicationFailure is thrown in the Update handler.
121
- *
124
+ * @throws {@link WorkflowUpdateRPCTimeoutOrCancelledError} if this Update call timed out or was cancelled. This doesn't
125
+ * mean the update itself was timed out or cancelled.
122
126
  * @param def an Update definition as returned from {@link defineUpdate}
123
127
  * @param options Update arguments
124
128
  *
@@ -138,30 +142,51 @@ export interface WorkflowHandle<T extends Workflow = Workflow> extends BaseWorkf
138
142
  ): Promise<Ret>;
139
143
 
140
144
  /**
141
- * Start an Update and receive a handle to the Update.
142
- * The Update validator (if present) is run before the handle is returned.
145
+ * Start an Update and receive a handle to the Update. The Update validator (if present) is run
146
+ * before the handle is returned.
143
147
  *
144
148
  * @experimental Update is an experimental feature.
145
149
  *
146
150
  * @throws {@link WorkflowUpdateFailedError} if Update validation fails.
151
+ * @throws {@link WorkflowUpdateRPCTimeoutOrCancelledError} if this Update call timed out or was cancelled. This doesn't
152
+ * mean the update itself was timed out or cancelled.
147
153
  *
148
154
  * @param def an Update definition as returned from {@link defineUpdate}
149
- * @param options Update arguments
155
+ * @param options update arguments, and update lifecycle stage to wait for
156
+ *
157
+ * Currently, startUpdate always waits until a worker is accepting tasks for the workflow and the
158
+ * update is accepted or rejected, and the options object must be at least
159
+ * ```ts
160
+ * {
161
+ * waitForStage: WorkflowUpdateStage.ACCEPTED
162
+ * }
163
+ * ```
164
+ * If the update takes arguments, then the options object must additionally contain an `args`
165
+ * property with an array of argument values.
150
166
  *
151
167
  * @example
152
168
  * ```ts
153
- * const updateHandle = await handle.startUpdate(incrementAndGetValueUpdate, { args: [2] });
169
+ * const updateHandle = await handle.startUpdate(incrementAndGetValueUpdate, {
170
+ * args: [2],
171
+ * waitForStage: WorkflowUpdateStage.ACCEPTED,
172
+ * });
154
173
  * const updateResult = await updateHandle.result();
155
174
  * ```
156
175
  */
157
176
  startUpdate<Ret, Args extends [any, ...any[]], Name extends string = string>(
158
177
  def: UpdateDefinition<Ret, Args, Name> | string,
159
- options: WorkflowUpdateOptions & { args: Args }
178
+ options: WorkflowUpdateOptions & {
179
+ args: Args;
180
+ waitForStage: WorkflowUpdateStage.ACCEPTED;
181
+ }
160
182
  ): Promise<WorkflowUpdateHandle<Ret>>;
161
183
 
162
184
  startUpdate<Ret, Args extends [], Name extends string = string>(
163
185
  def: UpdateDefinition<Ret, Args, Name> | string,
164
- options?: WorkflowUpdateOptions & { args?: Args }
186
+ options: WorkflowUpdateOptions & {
187
+ args?: Args;
188
+ waitForStage: WorkflowUpdateStage.ACCEPTED;
189
+ }
165
190
  ): Promise<WorkflowUpdateHandle<Ret>>;
166
191
 
167
192
  /**
@@ -692,6 +717,28 @@ export class WorkflowClient extends BaseClient {
692
717
  }
693
718
  }
694
719
 
720
+ protected rethrowUpdateGrpcError(
721
+ err: unknown,
722
+ fallbackMessage: string,
723
+ workflowExecution?: WorkflowExecution
724
+ ): never {
725
+ if (isGrpcServiceError(err)) {
726
+ if (err.code === grpcStatus.DEADLINE_EXCEEDED || err.code === grpcStatus.CANCELLED) {
727
+ throw new WorkflowUpdateRPCTimeoutOrCancelledError(err.details ?? 'Workflow update call timeout or cancelled', {
728
+ cause: err,
729
+ });
730
+ }
731
+ }
732
+
733
+ if (err instanceof CancelledFailure) {
734
+ throw new WorkflowUpdateRPCTimeoutOrCancelledError(err.message ?? 'Workflow update call timeout or cancelled', {
735
+ cause: err,
736
+ });
737
+ }
738
+
739
+ this.rethrowGrpcError(err, fallbackMessage, workflowExecution);
740
+ }
741
+
695
742
  protected rethrowGrpcError(err: unknown, fallbackMessage: string, workflowExecution?: WorkflowExecution): never {
696
743
  if (isGrpcServiceError(err)) {
697
744
  rethrowKnownErrorTypes(err);
@@ -756,15 +803,19 @@ export class WorkflowClient extends BaseClient {
756
803
  * Used as the final function of the interceptor chain during startUpdate and executeUpdate.
757
804
  */
758
805
  protected async _startUpdateHandler(
759
- waitForStage: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage,
806
+ waitForStage: WorkflowUpdateStage,
760
807
  input: WorkflowStartUpdateInput
761
808
  ): Promise<WorkflowStartUpdateOutput> {
809
+ waitForStage = waitForStage >= WorkflowUpdateStage.ACCEPTED ? waitForStage : WorkflowUpdateStage.ACCEPTED;
810
+ const waitForStageProto = workflowUpdateStage.toProtoEnum(waitForStage);
762
811
  const updateId = input.options?.updateId ?? uuid4();
763
812
  const req: temporal.api.workflowservice.v1.IUpdateWorkflowExecutionRequest = {
764
813
  namespace: this.options.namespace,
765
814
  workflowExecution: input.workflowExecution,
766
815
  firstExecutionRunId: input.firstExecutionRunId,
767
- waitPolicy: { lifecycleStage: waitForStage },
816
+ waitPolicy: {
817
+ lifecycleStage: waitForStageProto,
818
+ },
768
819
  request: {
769
820
  meta: {
770
821
  updateId,
@@ -777,12 +828,17 @@ export class WorkflowClient extends BaseClient {
777
828
  },
778
829
  },
779
830
  };
780
- let response: temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse;
781
831
 
832
+ // Repeatedly send UpdateWorkflowExecution until update is >= Accepted or >= `waitForStage` (if
833
+ // the server receives a request with an update ID that already exists, it responds with
834
+ // information for the existing update).
835
+ let response: temporal.api.workflowservice.v1.UpdateWorkflowExecutionResponse;
782
836
  try {
783
- response = await this.workflowService.updateWorkflowExecution(req);
837
+ do {
838
+ response = await this.workflowService.updateWorkflowExecution(req);
839
+ } while (response.stage < waitForStageProto);
784
840
  } catch (err) {
785
- this.rethrowGrpcError(err, 'Workflow Update failed', input.workflowExecution);
841
+ this.rethrowUpdateGrpcError(err, 'Workflow Update failed', input.workflowExecution);
786
842
  }
787
843
  return {
788
844
  updateId,
@@ -830,21 +886,9 @@ export class WorkflowClient extends BaseClient {
830
886
  updateRef: { workflowExecution, updateId },
831
887
  identity: this.options.identity,
832
888
  waitPolicy: {
833
- lifecycleStage:
834
- temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage
835
- .UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
889
+ lifecycleStage: workflowUpdateStage.toProtoEnum(WorkflowUpdateStage.COMPLETED),
836
890
  },
837
891
  };
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
892
  for (;;) {
849
893
  try {
850
894
  const response = await this.workflowService.pollWorkflowExecutionUpdate(req);
@@ -852,9 +896,8 @@ export class WorkflowClient extends BaseClient {
852
896
  return response.outcome;
853
897
  }
854
898
  } catch (err) {
855
- if (!(isGrpcServiceError(err) && err.code === grpcStatus.DEADLINE_EXCEEDED)) {
856
- throw err;
857
- }
899
+ const wE = typeof workflowExecution.workflowId === 'string' ? workflowExecution : undefined;
900
+ this.rethrowUpdateGrpcError(err, 'Workflow Update Poll failed', wE as WorkflowExecution);
858
901
  }
859
902
  }
860
903
  }
@@ -1050,10 +1093,9 @@ export class WorkflowClient extends BaseClient {
1050
1093
  runIdForResult,
1051
1094
  ...resultOptions
1052
1095
  }: WorkflowHandleOptions): WorkflowHandle<T> {
1053
- // TODO (dan): Convert to class with this as a protected method
1054
1096
  const _startUpdate = async <Ret, Args extends unknown[]>(
1055
1097
  def: UpdateDefinition<Ret, Args> | string,
1056
- waitForStage: temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage,
1098
+ waitForStage: WorkflowUpdateStage,
1057
1099
  options?: WorkflowUpdateOptions & { args?: Args }
1058
1100
  ): Promise<WorkflowUpdateHandle<Ret>> => {
1059
1101
  const next = this._startUpdateHandler.bind(this, waitForStage);
@@ -1069,12 +1111,16 @@ export class WorkflowClient extends BaseClient {
1069
1111
  options: opts,
1070
1112
  };
1071
1113
  const output = await fn(input);
1072
- return this.createWorkflowUpdateHandle<Ret>(
1114
+ const handle = this.createWorkflowUpdateHandle<Ret>(
1073
1115
  output.updateId,
1074
1116
  input.workflowExecution.workflowId,
1075
1117
  output.workflowRunId,
1076
1118
  output.outcome
1077
1119
  );
1120
+ if (!output.outcome && waitForStage === WorkflowUpdateStage.COMPLETED) {
1121
+ await this._pollForUpdateOutcome(handle.updateId, input.workflowExecution);
1122
+ }
1123
+ return handle;
1078
1124
  };
1079
1125
 
1080
1126
  return {
@@ -1130,25 +1176,18 @@ export class WorkflowClient extends BaseClient {
1130
1176
  },
1131
1177
  async startUpdate<Ret, Args extends any[]>(
1132
1178
  def: UpdateDefinition<Ret, Args> | string,
1133
- options?: WorkflowUpdateOptions & { args?: Args }
1179
+ options: WorkflowUpdateOptions & {
1180
+ args?: Args;
1181
+ waitForStage: WorkflowUpdateStage.ACCEPTED;
1182
+ }
1134
1183
  ): 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
- );
1184
+ return await _startUpdate(def, options.waitForStage, options);
1141
1185
  },
1142
1186
  async executeUpdate<Ret, Args extends any[]>(
1143
1187
  def: UpdateDefinition<Ret, Args> | string,
1144
1188
  options?: WorkflowUpdateOptions & { args?: Args }
1145
1189
  ): 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
- );
1190
+ const handle = await _startUpdate(def, WorkflowUpdateStage.COMPLETED, options);
1152
1191
  return await handle.result();
1153
1192
  },
1154
1193
  getUpdateHandle<Ret>(updateId: string): WorkflowUpdateHandle<Ret> {
@@ -1254,9 +1293,7 @@ export class WorkflowClient extends BaseClient {
1254
1293
  this._list(options),
1255
1294
  async ({ workflowId, runId }) => ({
1256
1295
  workflowId,
1257
- history: await this.getHandle(workflowId, runId)
1258
- .fetchHistory()
1259
- .catch((_) => undefined),
1296
+ history: await this.getHandle(workflowId, runId).fetchHistory(),
1260
1297
  }),
1261
1298
  { concurrency: intoHistoriesOptions?.concurrency ?? 5 }
1262
1299
  );
@@ -0,0 +1,33 @@
1
+ import { temporal } from '@temporalio/proto';
2
+ import { checkExtends } from '@temporalio/common/lib/type-helpers';
3
+
4
+ export enum WorkflowUpdateStage {
5
+ /** This is not an allowed value. */
6
+ UNSPECIFIED = 0,
7
+ /** Admitted stage. This stage is reached when the server accepts the update request. It is not
8
+ * allowed to wait for this stage when using startUpdate, since the update request has not yet
9
+ * been durably persisted at this stage. */
10
+ ADMITTED = 1,
11
+ /** Accepted stage. This stage is reached when a workflow has received the update and either
12
+ * accepted it (i.e. it has passed validation, or there was no validator configured on the update
13
+ * handler) or rejected it. This is currently the only allowed value when using startUpdate. */
14
+ ACCEPTED = 2,
15
+ /** Completed stage. This stage is reached when a workflow has completed processing the
16
+ * update with either a success or failure. */
17
+ COMPLETED = 3,
18
+ }
19
+
20
+ checkExtends<
21
+ `UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_${keyof typeof WorkflowUpdateStage}`,
22
+ keyof typeof temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage
23
+ >();
24
+ checkExtends<
25
+ keyof typeof temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage,
26
+ `UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_${keyof typeof WorkflowUpdateStage}`
27
+ >();
28
+
29
+ export function toProtoEnum(stage: WorkflowUpdateStage): temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage {
30
+ return temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage[
31
+ `UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_${WorkflowUpdateStage[stage] as keyof typeof WorkflowUpdateStage}`
32
+ ];
33
+ }