@rivetkit/workflow-engine 2.1.11-rc.1 → 2.2.1

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/context.ts CHANGED
@@ -54,7 +54,16 @@ import type {
54
54
  RollbackContextInterface,
55
55
  StepConfig,
56
56
  Storage,
57
+ TryBlockCatchKind,
58
+ TryBlockConfig,
59
+ TryBlockFailure,
60
+ TryBlockResult,
61
+ TryStepCatchKind,
62
+ TryStepConfig,
63
+ TryStepFailure,
64
+ TryStepResult,
57
65
  WorkflowContextInterface,
66
+ WorkflowError,
58
67
  WorkflowErrorEvent,
59
68
  WorkflowErrorHandler,
60
69
  WorkflowQueue,
@@ -74,8 +83,20 @@ export const DEFAULT_RETRY_BACKOFF_BASE = 100;
74
83
  export const DEFAULT_RETRY_BACKOFF_MAX = 30000;
75
84
  export const DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL = 20;
76
85
  export const DEFAULT_STEP_TIMEOUT = 30000; // 30 seconds
86
+ const DEFAULT_TRY_STEP_CATCH: readonly TryStepCatchKind[] = [
87
+ "critical",
88
+ "timeout",
89
+ "exhausted",
90
+ ];
91
+ const DEFAULT_TRY_BLOCK_CATCH: readonly TryBlockCatchKind[] = [
92
+ "step",
93
+ "join",
94
+ "race",
95
+ ];
77
96
 
78
97
  const QUEUE_HISTORY_MESSAGE_MARKER = "__rivetWorkflowQueueMessage";
98
+ const TRY_STEP_FAILURE_SYMBOL = Symbol("workflow.try-step.failure");
99
+ const TRY_BLOCK_FAILURE_SYMBOL = Symbol("workflow.try-block.failure");
79
100
 
80
101
  /**
81
102
  * Calculate backoff delay with exponential backoff.
@@ -99,6 +120,215 @@ export class StepTimeoutError extends Error {
99
120
  }
100
121
  }
101
122
 
123
+ type SchedulerYieldState = {
124
+ deadline?: number;
125
+ messageNames: Set<string>;
126
+ };
127
+
128
+ type TryBlockFailureInfo = Pick<TryBlockFailure, "source" | "name">;
129
+
130
+ function attachTryStepFailure<T extends Error>(
131
+ error: T,
132
+ failure: TryStepFailure,
133
+ ): T {
134
+ (
135
+ error as T & {
136
+ [TRY_STEP_FAILURE_SYMBOL]?: TryStepFailure;
137
+ }
138
+ )[TRY_STEP_FAILURE_SYMBOL] = failure;
139
+ return error;
140
+ }
141
+
142
+ function readTryStepFailure(error: unknown): TryStepFailure | undefined {
143
+ if (!(error instanceof Error)) {
144
+ return undefined;
145
+ }
146
+
147
+ return (
148
+ error as Error & {
149
+ [TRY_STEP_FAILURE_SYMBOL]?: TryStepFailure;
150
+ }
151
+ )[TRY_STEP_FAILURE_SYMBOL];
152
+ }
153
+
154
+ function attachTryBlockFailure<T extends Error>(
155
+ error: T,
156
+ failure: TryBlockFailureInfo,
157
+ ): T {
158
+ (
159
+ error as T & {
160
+ [TRY_BLOCK_FAILURE_SYMBOL]?: TryBlockFailureInfo;
161
+ }
162
+ )[TRY_BLOCK_FAILURE_SYMBOL] = failure;
163
+ return error;
164
+ }
165
+
166
+ function readTryBlockFailure(error: unknown): TryBlockFailureInfo | undefined {
167
+ if (!(error instanceof Error)) {
168
+ return undefined;
169
+ }
170
+
171
+ return (
172
+ error as Error & {
173
+ [TRY_BLOCK_FAILURE_SYMBOL]?: TryBlockFailureInfo;
174
+ }
175
+ )[TRY_BLOCK_FAILURE_SYMBOL];
176
+ }
177
+
178
+ function shouldRethrowTryError(error: unknown): boolean {
179
+ return (
180
+ error instanceof StepFailedError ||
181
+ error instanceof SleepError ||
182
+ error instanceof MessageWaitError ||
183
+ error instanceof EvictedError ||
184
+ error instanceof HistoryDivergedError ||
185
+ error instanceof EntryInProgressError ||
186
+ error instanceof RollbackCheckpointError ||
187
+ error instanceof RollbackStopError
188
+ );
189
+ }
190
+
191
+ function shouldCatchTryStepFailure(
192
+ failure: TryStepFailure,
193
+ catchKinds?: readonly TryStepCatchKind[],
194
+ ): boolean {
195
+ const effectiveCatch = catchKinds ?? DEFAULT_TRY_STEP_CATCH;
196
+ return effectiveCatch.includes(failure.kind);
197
+ }
198
+
199
+ function shouldCatchTryBlockFailure(
200
+ failure: TryBlockFailure,
201
+ catchKinds?: readonly TryBlockCatchKind[],
202
+ ): boolean {
203
+ const effectiveCatch = catchKinds ?? DEFAULT_TRY_BLOCK_CATCH;
204
+
205
+ if (failure.source === "step") {
206
+ return failure.step?.kind === "rollback"
207
+ ? effectiveCatch.includes("rollback")
208
+ : effectiveCatch.includes("step");
209
+ }
210
+ if (failure.source === "join") {
211
+ return effectiveCatch.includes("join");
212
+ }
213
+ if (failure.source === "race") {
214
+ return effectiveCatch.includes("race");
215
+ }
216
+ return effectiveCatch.includes("rollback");
217
+ }
218
+
219
+ function parseStoredWorkflowError(message: string | undefined): WorkflowError {
220
+ if (!message) {
221
+ return {
222
+ name: "Error",
223
+ message: "unknown error",
224
+ };
225
+ }
226
+
227
+ const match = /^([^:]+):\s*(.*)$/s.exec(message);
228
+ if (!match) {
229
+ return {
230
+ name: "Error",
231
+ message,
232
+ };
233
+ }
234
+
235
+ return {
236
+ name: match[1],
237
+ message: match[2],
238
+ };
239
+ }
240
+
241
+ function getTryStepFailureFromExhaustedError(
242
+ stepName: string,
243
+ attempts: number,
244
+ error: StepExhaustedError,
245
+ ): TryStepFailure {
246
+ return {
247
+ kind: "exhausted",
248
+ stepName,
249
+ attempts,
250
+ error: parseStoredWorkflowError(error.lastError),
251
+ };
252
+ }
253
+
254
+ function mergeSchedulerYield(
255
+ state: SchedulerYieldState | undefined,
256
+ error: SleepError | MessageWaitError | StepFailedError,
257
+ ): SchedulerYieldState {
258
+ const nextState: SchedulerYieldState = state ?? {
259
+ messageNames: new Set<string>(),
260
+ };
261
+
262
+ if (error instanceof SleepError) {
263
+ nextState.deadline =
264
+ nextState.deadline === undefined
265
+ ? error.deadline
266
+ : Math.min(nextState.deadline, error.deadline);
267
+ for (const messageName of error.messageNames ?? []) {
268
+ nextState.messageNames.add(messageName);
269
+ }
270
+ return nextState;
271
+ }
272
+
273
+ if (error instanceof MessageWaitError) {
274
+ for (const messageName of error.messageNames) {
275
+ nextState.messageNames.add(messageName);
276
+ }
277
+ return nextState;
278
+ }
279
+
280
+ nextState.deadline =
281
+ nextState.deadline === undefined
282
+ ? error.retryAt
283
+ : Math.min(nextState.deadline, error.retryAt);
284
+ return nextState;
285
+ }
286
+
287
+ function buildSchedulerYieldError(
288
+ state: SchedulerYieldState,
289
+ ): SleepError | MessageWaitError {
290
+ const messageNames = [...state.messageNames];
291
+ if (state.deadline !== undefined) {
292
+ return new SleepError(
293
+ state.deadline,
294
+ messageNames.length > 0 ? messageNames : undefined,
295
+ );
296
+ }
297
+ return new MessageWaitError(messageNames);
298
+ }
299
+
300
+ function controlFlowErrorPriority(error: Error): number {
301
+ if (error instanceof EvictedError) {
302
+ return 0;
303
+ }
304
+ if (error instanceof HistoryDivergedError) {
305
+ return 1;
306
+ }
307
+ if (error instanceof EntryInProgressError) {
308
+ return 2;
309
+ }
310
+ if (error instanceof RollbackCheckpointError) {
311
+ return 3;
312
+ }
313
+ if (error instanceof RollbackStopError) {
314
+ return 4;
315
+ }
316
+ return 5;
317
+ }
318
+
319
+ function selectControlFlowError(
320
+ current: Error | undefined,
321
+ candidate: Error,
322
+ ): Error {
323
+ if (!current) {
324
+ return candidate;
325
+ }
326
+ return controlFlowErrorPriority(candidate) <
327
+ controlFlowErrorPriority(current)
328
+ ? candidate
329
+ : current;
330
+ }
331
+
102
332
  /**
103
333
  * Internal representation of a rollback handler.
104
334
  */
@@ -416,6 +646,138 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
416
646
  }
417
647
  }
418
648
 
649
+ async tryStep<T>(
650
+ nameOrConfig: string | TryStepConfig<T>,
651
+ run?: () => Promise<T>,
652
+ ): Promise<TryStepResult<T>> {
653
+ const config =
654
+ typeof nameOrConfig === "string"
655
+ ? ({
656
+ name: nameOrConfig,
657
+ run: run!,
658
+ } satisfies TryStepConfig<T>)
659
+ : nameOrConfig;
660
+
661
+ try {
662
+ return {
663
+ ok: true,
664
+ value: await this.step(config),
665
+ };
666
+ } catch (error) {
667
+ if (shouldRethrowTryError(error)) {
668
+ throw error;
669
+ }
670
+
671
+ const failure = readTryStepFailure(error);
672
+ if (!failure || !shouldCatchTryStepFailure(failure, config.catch)) {
673
+ throw error;
674
+ }
675
+
676
+ return {
677
+ ok: false,
678
+ failure,
679
+ };
680
+ }
681
+ }
682
+
683
+ async try<T>(
684
+ nameOrConfig: string | TryBlockConfig<T>,
685
+ run?: (ctx: WorkflowContextInterface) => Promise<T>,
686
+ ): Promise<TryBlockResult<T>> {
687
+ this.assertNotInProgress();
688
+ this.checkEvicted();
689
+
690
+ const config =
691
+ typeof nameOrConfig === "string"
692
+ ? ({
693
+ name: nameOrConfig,
694
+ run: run!,
695
+ } satisfies TryBlockConfig<T>)
696
+ : nameOrConfig;
697
+
698
+ this.entryInProgress = true;
699
+ try {
700
+ return await this.executeTry(config);
701
+ } finally {
702
+ this.entryInProgress = false;
703
+ }
704
+ }
705
+
706
+ private async executeTry<T>(
707
+ config: TryBlockConfig<T>,
708
+ ): Promise<TryBlockResult<T>> {
709
+ this.checkDuplicateName(config.name);
710
+
711
+ const location = appendName(
712
+ this.storage,
713
+ this.currentLocation,
714
+ config.name,
715
+ );
716
+ const blockCtx = this.createBranch(location);
717
+
718
+ try {
719
+ const value = await config.run(blockCtx);
720
+ blockCtx.validateComplete();
721
+ return {
722
+ ok: true,
723
+ value,
724
+ };
725
+ } catch (error) {
726
+ if (shouldRethrowTryError(error)) {
727
+ throw error;
728
+ }
729
+
730
+ const stepFailure = readTryStepFailure(error);
731
+ if (stepFailure) {
732
+ const failure: TryBlockFailure = {
733
+ source: "step",
734
+ name: stepFailure.stepName,
735
+ error: stepFailure.error,
736
+ step: stepFailure,
737
+ };
738
+ if (!shouldCatchTryBlockFailure(failure, config.catch)) {
739
+ throw error;
740
+ }
741
+ return {
742
+ ok: false,
743
+ failure,
744
+ };
745
+ }
746
+
747
+ const operationFailure = readTryBlockFailure(error);
748
+ if (operationFailure) {
749
+ const failure: TryBlockFailure = {
750
+ ...operationFailure,
751
+ error: extractErrorInfo(error),
752
+ };
753
+ if (!shouldCatchTryBlockFailure(failure, config.catch)) {
754
+ throw error;
755
+ }
756
+ return {
757
+ ok: false,
758
+ failure,
759
+ };
760
+ }
761
+
762
+ if (error instanceof RollbackError) {
763
+ const failure: TryBlockFailure = {
764
+ source: "block",
765
+ name: config.name,
766
+ error: extractErrorInfo(error),
767
+ };
768
+ if (!shouldCatchTryBlockFailure(failure, config.catch)) {
769
+ throw error;
770
+ }
771
+ return {
772
+ ok: false,
773
+ failure,
774
+ };
775
+ }
776
+
777
+ throw error;
778
+ }
779
+ }
780
+
419
781
  private async executeStep<T>(config: StepConfig<T>): Promise<T> {
420
782
  this.ensureRollbackCheckpoint(config);
421
783
  if (this.mode === "rollback") {
@@ -467,9 +829,19 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
467
829
  // driver implementations may persist metadata without the history
468
830
  // entry error (e.g. partial writes/crashes between attempts).
469
831
  const lastError = stepData.error ?? metadata.error;
470
- const exhaustedError = markErrorReported(
471
- new StepExhaustedError(config.name, lastError),
832
+ const exhaustedError = new StepExhaustedError(
833
+ config.name,
834
+ lastError,
835
+ );
836
+ attachTryStepFailure(
837
+ exhaustedError,
838
+ getTryStepFailureFromExhaustedError(
839
+ config.name,
840
+ metadata.attempts,
841
+ exhaustedError,
842
+ ),
472
843
  );
844
+ markErrorReported(exhaustedError);
473
845
  if (metadata.status !== "exhausted") {
474
846
  metadata.status = "exhausted";
475
847
  metadata.dirty = true;
@@ -581,7 +953,17 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
581
953
  await this.notifyStepError(config, metadata.attempts, error, {
582
954
  willRetry: false,
583
955
  });
584
- throw markErrorReported(new CriticalError(error.message));
956
+ throw markErrorReported(
957
+ attachTryStepFailure(
958
+ new CriticalError(error.message),
959
+ {
960
+ kind: "timeout",
961
+ stepName: config.name,
962
+ attempts: metadata.attempts,
963
+ error: extractErrorInfo(error),
964
+ },
965
+ ),
966
+ );
585
967
  }
586
968
 
587
969
  if (
@@ -598,7 +980,17 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
598
980
  await this.notifyStepError(config, metadata.attempts, error, {
599
981
  willRetry: false,
600
982
  });
601
- throw markErrorReported(error);
983
+ throw markErrorReported(
984
+ attachTryStepFailure(error, {
985
+ kind:
986
+ error instanceof RollbackError
987
+ ? "rollback"
988
+ : "critical",
989
+ stepName: config.name,
990
+ attempts: metadata.attempts,
991
+ error: extractErrorInfo(error),
992
+ }),
993
+ );
602
994
  }
603
995
 
604
996
  if (entry.kind.type === "step") {
@@ -631,7 +1023,15 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
631
1023
  }
632
1024
 
633
1025
  const exhaustedError = markErrorReported(
634
- new StepExhaustedError(config.name, String(error)),
1026
+ attachTryStepFailure(
1027
+ new StepExhaustedError(config.name, String(error)),
1028
+ {
1029
+ kind: "exhausted",
1030
+ stepName: config.name,
1031
+ attempts: metadata.attempts,
1032
+ error: extractErrorInfo(error),
1033
+ },
1034
+ ),
635
1035
  );
636
1036
  await this.notifyStepError(config, metadata.attempts, error, {
637
1037
  willRetry: false,
@@ -1657,11 +2057,33 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1657
2057
  const joinData = entry.kind.data;
1658
2058
  const results: Record<string, unknown> = {};
1659
2059
  const errors: Record<string, Error> = {};
2060
+ let schedulerYieldState: SchedulerYieldState | undefined;
2061
+ let propagatedError: Error | undefined;
2062
+
2063
+ for (const [branchName, branchStatus] of Object.entries(
2064
+ joinData.branches,
2065
+ )) {
2066
+ if (branchStatus.status === "completed") {
2067
+ results[branchName] = branchStatus.output;
2068
+ continue;
2069
+ }
2070
+
2071
+ if (branchStatus.status === "failed") {
2072
+ errors[branchName] = new Error(
2073
+ branchStatus.error ?? "branch failed",
2074
+ );
2075
+ }
2076
+ }
1660
2077
 
1661
2078
  // Execute all branches in parallel
1662
2079
  const branchPromises = Object.entries(branches).map(
1663
2080
  async ([branchName, config]) => {
1664
2081
  const branchStatus = joinData.branches[branchName];
2082
+ if (!branchStatus) {
2083
+ throw new HistoryDivergedError(
2084
+ `Expected join branch "${branchName}" in "${name}"`,
2085
+ );
2086
+ }
1665
2087
 
1666
2088
  // Already completed
1667
2089
  if (branchStatus.status === "completed") {
@@ -1684,6 +2106,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1684
2106
  const branchCtx = this.createBranch(branchLocation);
1685
2107
 
1686
2108
  branchStatus.status = "running";
2109
+ branchStatus.error = undefined;
1687
2110
  entry.dirty = true;
1688
2111
 
1689
2112
  try {
@@ -1692,9 +2115,43 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1692
2115
 
1693
2116
  branchStatus.status = "completed";
1694
2117
  branchStatus.output = output;
2118
+ branchStatus.error = undefined;
1695
2119
  results[branchName] = output;
1696
2120
  } catch (error) {
2121
+ if (
2122
+ error instanceof SleepError ||
2123
+ error instanceof MessageWaitError ||
2124
+ error instanceof StepFailedError
2125
+ ) {
2126
+ schedulerYieldState = mergeSchedulerYield(
2127
+ schedulerYieldState,
2128
+ error,
2129
+ );
2130
+ branchStatus.status = "running";
2131
+ branchStatus.error = undefined;
2132
+ entry.dirty = true;
2133
+ return;
2134
+ }
2135
+
2136
+ if (
2137
+ error instanceof EvictedError ||
2138
+ error instanceof HistoryDivergedError ||
2139
+ error instanceof EntryInProgressError ||
2140
+ error instanceof RollbackCheckpointError ||
2141
+ error instanceof RollbackStopError
2142
+ ) {
2143
+ propagatedError = selectControlFlowError(
2144
+ propagatedError,
2145
+ error,
2146
+ );
2147
+ branchStatus.status = "running";
2148
+ branchStatus.error = undefined;
2149
+ entry.dirty = true;
2150
+ return;
2151
+ }
2152
+
1697
2153
  branchStatus.status = "failed";
2154
+ branchStatus.output = undefined;
1698
2155
  branchStatus.error = String(error);
1699
2156
  errors[branchName] = error as Error;
1700
2157
  }
@@ -1707,9 +2164,30 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1707
2164
  await Promise.allSettled(branchPromises);
1708
2165
  await this.flushStorage();
1709
2166
 
2167
+ if (propagatedError) {
2168
+ throw propagatedError;
2169
+ }
2170
+
2171
+ if (
2172
+ Object.values(joinData.branches).some(
2173
+ (branch) =>
2174
+ branch.status === "pending" || branch.status === "running",
2175
+ )
2176
+ ) {
2177
+ if (!schedulerYieldState) {
2178
+ throw new Error(
2179
+ `Join "${name}" has pending branches without a scheduler yield`,
2180
+ );
2181
+ }
2182
+ throw buildSchedulerYieldError(schedulerYieldState);
2183
+ }
2184
+
1710
2185
  // Throw if any branches failed
1711
2186
  if (Object.keys(errors).length > 0) {
1712
- throw new JoinError(errors);
2187
+ throw attachTryBlockFailure(new JoinError(errors), {
2188
+ source: "join",
2189
+ name,
2190
+ });
1713
2191
  }
1714
2192
 
1715
2193
  return results as { [K in keyof T]: BranchOutput<T[K]> };
@@ -1809,24 +2287,32 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1809
2287
 
1810
2288
  // Track winner info
1811
2289
  let winnerName: string | null = null;
1812
- let winnerValue: T | null = null;
1813
- let settled = false;
1814
- let pendingCount = branches.length;
2290
+ let winnerValue: T | undefined;
2291
+ let hasWinner = false;
1815
2292
  const errors: Record<string, Error> = {};
1816
2293
  const lateErrors: Array<{ name: string; error: string }> = [];
1817
- // Track scheduler yield errors - we need to propagate these after allSettled
1818
- let yieldError: SleepError | MessageWaitError | null = null;
2294
+ let schedulerYieldState: SchedulerYieldState | undefined;
2295
+ let propagatedError: Error | undefined;
1819
2296
 
1820
2297
  // Check for replay winners first
1821
2298
  for (const branch of branches) {
1822
2299
  const branchStatus = raceData.branches[branch.name];
2300
+ if (!branchStatus) {
2301
+ throw new HistoryDivergedError(
2302
+ `Expected race branch "${branch.name}" in "${name}"`,
2303
+ );
2304
+ }
1823
2305
  if (
1824
2306
  branchStatus.status !== "pending" &&
1825
2307
  branchStatus.status !== "running"
1826
2308
  ) {
1827
- pendingCount--;
1828
- if (branchStatus.status === "completed" && !settled) {
1829
- settled = true;
2309
+ if (branchStatus.status === "failed") {
2310
+ errors[branch.name] = new Error(
2311
+ branchStatus.error ?? "branch failed",
2312
+ );
2313
+ }
2314
+ if (branchStatus.status === "completed" && !hasWinner) {
2315
+ hasWinner = true;
1830
2316
  winnerName = branch.name;
1831
2317
  winnerValue = branchStatus.output as T;
1832
2318
  }
@@ -1834,13 +2320,18 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1834
2320
  }
1835
2321
 
1836
2322
  // If we found a replay winner, return immediately
1837
- if (settled && winnerName !== null && winnerValue !== null) {
1838
- return { winner: winnerName, value: winnerValue };
2323
+ if (hasWinner && winnerName !== null) {
2324
+ return { winner: winnerName, value: winnerValue as T };
1839
2325
  }
1840
2326
 
1841
2327
  // Execute branches that need to run
1842
2328
  for (const branch of branches) {
1843
2329
  const branchStatus = raceData.branches[branch.name];
2330
+ if (!branchStatus) {
2331
+ throw new HistoryDivergedError(
2332
+ `Expected race branch "${branch.name}" in "${name}"`,
2333
+ );
2334
+ }
1844
2335
 
1845
2336
  // Skip already completed/cancelled
1846
2337
  if (
@@ -1861,19 +2352,30 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1861
2352
  );
1862
2353
 
1863
2354
  branchStatus.status = "running";
2355
+ branchStatus.error = undefined;
1864
2356
  entry.dirty = true;
1865
2357
 
1866
2358
  const branchPromise = branch.run(branchCtx).then(
1867
2359
  async (output) => {
1868
- if (settled) {
2360
+ if (hasWinner) {
1869
2361
  // This branch completed after a winner was determined
1870
2362
  // Still record the completion for observability
1871
2363
  branchStatus.status = "completed";
1872
2364
  branchStatus.output = output;
2365
+ branchStatus.error = undefined;
2366
+ entry.dirty = true;
2367
+ return;
2368
+ }
2369
+
2370
+ if (propagatedError) {
2371
+ branchStatus.status = "completed";
2372
+ branchStatus.output = output;
2373
+ branchStatus.error = undefined;
1873
2374
  entry.dirty = true;
1874
2375
  return;
1875
2376
  }
1876
- settled = true;
2377
+
2378
+ hasWinner = true;
1877
2379
  winnerName = branch.name;
1878
2380
  winnerValue = output;
1879
2381
 
@@ -1881,6 +2383,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1881
2383
 
1882
2384
  branchStatus.status = "completed";
1883
2385
  branchStatus.output = output;
2386
+ branchStatus.error = undefined;
1884
2387
  raceData.winner = branch.name;
1885
2388
  entry.dirty = true;
1886
2389
 
@@ -1888,69 +2391,63 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1888
2391
  raceAbortController.abort();
1889
2392
  },
1890
2393
  (error) => {
1891
- pendingCount--;
1892
-
1893
- // Track sleep/message errors - they need to bubble up to the scheduler
1894
- // We'll re-throw after allSettled to allow cleanup
1895
- if (error instanceof SleepError) {
1896
- // Track the earliest deadline
2394
+ if (hasWinner) {
1897
2395
  if (
1898
- !yieldError ||
1899
- !(yieldError instanceof SleepError) ||
1900
- error.deadline < yieldError.deadline
2396
+ error instanceof CancelledError ||
2397
+ error instanceof EvictedError
1901
2398
  ) {
1902
- yieldError = error;
2399
+ branchStatus.status = "cancelled";
2400
+ } else {
2401
+ lateErrors.push({
2402
+ name: branch.name,
2403
+ error: String(error),
2404
+ });
1903
2405
  }
1904
- branchStatus.status = "running"; // Keep as running since we'll resume
1905
2406
  entry.dirty = true;
1906
2407
  return;
1907
2408
  }
1908
- if (error instanceof MessageWaitError) {
1909
- // Track message wait errors, prefer sleep errors with deadlines
1910
- if (
1911
- !yieldError ||
1912
- !(yieldError instanceof SleepError)
1913
- ) {
1914
- if (!yieldError) {
1915
- yieldError = error;
1916
- } else if (yieldError instanceof MessageWaitError) {
1917
- // Merge message names
1918
- yieldError = new MessageWaitError([
1919
- ...yieldError.messageNames,
1920
- ...error.messageNames,
1921
- ]);
1922
- }
1923
- }
1924
- branchStatus.status = "running"; // Keep as running since we'll resume
2409
+
2410
+ if (
2411
+ error instanceof SleepError ||
2412
+ error instanceof MessageWaitError ||
2413
+ error instanceof StepFailedError
2414
+ ) {
2415
+ schedulerYieldState = mergeSchedulerYield(
2416
+ schedulerYieldState,
2417
+ error,
2418
+ );
2419
+ branchStatus.status = "running";
2420
+ branchStatus.error = undefined;
1925
2421
  entry.dirty = true;
1926
2422
  return;
1927
2423
  }
1928
2424
 
1929
2425
  if (
1930
- error instanceof CancelledError ||
1931
- error instanceof EvictedError
2426
+ error instanceof EvictedError ||
2427
+ error instanceof HistoryDivergedError ||
2428
+ error instanceof EntryInProgressError ||
2429
+ error instanceof RollbackCheckpointError ||
2430
+ error instanceof RollbackStopError
1932
2431
  ) {
2432
+ propagatedError = selectControlFlowError(
2433
+ propagatedError,
2434
+ error,
2435
+ );
2436
+ branchStatus.status = "running";
2437
+ branchStatus.error = undefined;
2438
+ entry.dirty = true;
2439
+ return;
2440
+ }
2441
+
2442
+ if (error instanceof CancelledError) {
1933
2443
  branchStatus.status = "cancelled";
1934
2444
  } else {
1935
2445
  branchStatus.status = "failed";
2446
+ branchStatus.output = undefined;
1936
2447
  branchStatus.error = String(error);
1937
-
1938
- if (settled) {
1939
- // Track late errors for observability
1940
- lateErrors.push({
1941
- name: branch.name,
1942
- error: String(error),
1943
- });
1944
- } else {
1945
- errors[branch.name] = error;
1946
- }
2448
+ errors[branch.name] = error as Error;
1947
2449
  }
1948
2450
  entry.dirty = true;
1949
-
1950
- // All branches failed (only if no winner yet)
1951
- if (pendingCount === 0 && !settled) {
1952
- settled = true;
1953
- }
1954
2451
  },
1955
2452
  );
1956
2453
 
@@ -1960,15 +2457,29 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1960
2457
  // Wait for all branches to complete or be cancelled
1961
2458
  await Promise.allSettled(branchPromises);
1962
2459
 
1963
- // If any branch needs to yield to the scheduler (sleep/message wait),
1964
- // save state and re-throw the error to exit the workflow execution
1965
- if (yieldError && !settled) {
2460
+ if (propagatedError) {
1966
2461
  await this.flushStorage();
1967
- throw yieldError;
2462
+ throw propagatedError;
2463
+ }
2464
+
2465
+ if (
2466
+ !hasWinner &&
2467
+ Object.values(raceData.branches).some(
2468
+ (branch) =>
2469
+ branch.status === "pending" || branch.status === "running",
2470
+ )
2471
+ ) {
2472
+ await this.flushStorage();
2473
+ if (!schedulerYieldState) {
2474
+ throw new Error(
2475
+ `Race "${name}" has pending branches without a scheduler yield`,
2476
+ );
2477
+ }
2478
+ throw buildSchedulerYieldError(schedulerYieldState);
1968
2479
  }
1969
2480
 
1970
2481
  // Clean up entries from non-winning branches
1971
- if (winnerName !== null) {
2482
+ if (hasWinner && winnerName !== null) {
1972
2483
  for (const branch of branches) {
1973
2484
  if (branch.name !== winnerName) {
1974
2485
  const branchLocation = appendName(
@@ -1998,17 +2509,23 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1998
2509
  }
1999
2510
 
2000
2511
  // Return result or throw error
2001
- if (winnerName !== null && winnerValue !== null) {
2002
- return { winner: winnerName, value: winnerValue };
2512
+ if (hasWinner && winnerName !== null) {
2513
+ return { winner: winnerName, value: winnerValue as T };
2003
2514
  }
2004
2515
 
2005
2516
  // All branches failed
2006
- throw new RaceError(
2007
- "All branches failed",
2008
- Object.entries(errors).map(([name, error]) => ({
2517
+ throw attachTryBlockFailure(
2518
+ new RaceError(
2519
+ "All branches failed",
2520
+ Object.entries(errors).map(([branchName, error]) => ({
2521
+ name: branchName,
2522
+ error: String(error),
2523
+ })),
2524
+ ),
2525
+ {
2526
+ source: "race",
2009
2527
  name,
2010
- error: String(error),
2011
- })),
2528
+ },
2012
2529
  );
2013
2530
  }
2014
2531