@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/dist/tsup/{chunk-OYYWSC77.cjs → chunk-4SWXLWKL.cjs} +426 -54
- package/dist/tsup/chunk-4SWXLWKL.cjs.map +1 -0
- package/dist/tsup/{chunk-4ME2JBMC.js → chunk-UMFB2AR3.js} +426 -54
- package/dist/tsup/chunk-UMFB2AR3.js.map +1 -0
- package/dist/tsup/index.cjs +2 -2
- package/dist/tsup/index.d.cts +44 -1
- package/dist/tsup/index.d.ts +44 -1
- package/dist/tsup/index.js +1 -1
- package/dist/tsup/testing.cjs +23 -23
- package/dist/tsup/testing.d.cts +1 -1
- package/dist/tsup/testing.d.ts +1 -1
- package/dist/tsup/testing.js +1 -1
- package/package.json +1 -1
- package/src/context.ts +592 -75
- package/src/index.ts +8 -0
- package/src/types.ts +53 -0
- package/dist/tsup/chunk-4ME2JBMC.js.map +0 -1
- package/dist/tsup/chunk-OYYWSC77.cjs.map +0 -1
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 =
|
|
471
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 |
|
|
1813
|
-
let
|
|
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
|
-
|
|
1818
|
-
let
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
error.deadline < yieldError.deadline
|
|
2396
|
+
error instanceof CancelledError ||
|
|
2397
|
+
error instanceof EvictedError
|
|
1901
2398
|
) {
|
|
1902
|
-
|
|
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
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
|
1931
|
-
error instanceof
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
|
|
2011
|
-
})),
|
|
2528
|
+
},
|
|
2012
2529
|
);
|
|
2013
2530
|
}
|
|
2014
2531
|
|