@rivetkit/workflow-engine 2.1.6 → 2.1.8
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-JTLDEP6X.js → chunk-4ME2JBMC.js} +542 -210
- package/dist/tsup/chunk-4ME2JBMC.js.map +1 -0
- package/dist/tsup/{chunk-KQO2TD7T.cjs → chunk-OYYWSC77.cjs} +539 -207
- package/dist/tsup/chunk-OYYWSC77.cjs.map +1 -0
- package/dist/tsup/index.cjs +2 -2
- package/dist/tsup/index.cjs.map +1 -1
- package/dist/tsup/index.d.cts +93 -25
- package/dist/tsup/index.d.ts +93 -25
- package/dist/tsup/index.js +7 -7
- package/dist/tsup/testing.cjs +35 -23
- package/dist/tsup/testing.cjs.map +1 -1
- package/dist/tsup/testing.d.cts +2 -1
- package/dist/tsup/testing.d.ts +2 -1
- package/dist/tsup/testing.js +21 -9
- package/dist/tsup/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/context.ts +289 -113
- package/src/driver.ts +5 -0
- package/src/error-utils.ts +87 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +214 -55
- package/src/keys.ts +26 -0
- package/src/location.ts +4 -1
- package/src/storage.ts +73 -20
- package/src/testing.ts +25 -4
- package/src/types.ts +48 -11
- package/dist/tsup/chunk-JTLDEP6X.js.map +0 -1
- package/dist/tsup/chunk-KQO2TD7T.cjs.map +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { WorkflowError, WorkflowErrorEvent } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const WORKFLOW_ERROR_REPORTED_SYMBOL = Symbol("workflow.error.reported");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract structured error information from an error.
|
|
7
|
+
*/
|
|
8
|
+
export function extractErrorInfo(error: unknown): WorkflowError {
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
const result: WorkflowError = {
|
|
11
|
+
name: error.name,
|
|
12
|
+
message: error.message,
|
|
13
|
+
stack: error.stack,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const metadata: Record<string, unknown> = {};
|
|
17
|
+
for (const key of Object.keys(error)) {
|
|
18
|
+
if (key !== "name" && key !== "message" && key !== "stack") {
|
|
19
|
+
const value = (error as unknown as Record<string, unknown>)[
|
|
20
|
+
key
|
|
21
|
+
];
|
|
22
|
+
if (
|
|
23
|
+
typeof value === "string" ||
|
|
24
|
+
typeof value === "number" ||
|
|
25
|
+
typeof value === "boolean" ||
|
|
26
|
+
value === null
|
|
27
|
+
) {
|
|
28
|
+
metadata[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(metadata).length > 0) {
|
|
33
|
+
result.metadata = metadata;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
name: "Error",
|
|
41
|
+
message: String(error),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mark an error after it has been reported to the error hook.
|
|
47
|
+
*/
|
|
48
|
+
export function markErrorReported<T extends Error>(error: T): T {
|
|
49
|
+
(
|
|
50
|
+
error as T & {
|
|
51
|
+
[WORKFLOW_ERROR_REPORTED_SYMBOL]?: boolean;
|
|
52
|
+
}
|
|
53
|
+
)[WORKFLOW_ERROR_REPORTED_SYMBOL] = true;
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if an error was already reported to the error hook.
|
|
59
|
+
*/
|
|
60
|
+
export function isErrorReported(error: unknown): boolean {
|
|
61
|
+
if (!(error instanceof Error)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Boolean(
|
|
66
|
+
(
|
|
67
|
+
error as Error & {
|
|
68
|
+
[WORKFLOW_ERROR_REPORTED_SYMBOL]?: boolean;
|
|
69
|
+
}
|
|
70
|
+
)[WORKFLOW_ERROR_REPORTED_SYMBOL],
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Return the outer tag name for a workflow error event.
|
|
76
|
+
*/
|
|
77
|
+
export function getErrorEventTag(
|
|
78
|
+
event: WorkflowErrorEvent,
|
|
79
|
+
): "step" | "rollback" | "workflow" {
|
|
80
|
+
if ("step" in event) {
|
|
81
|
+
return "step";
|
|
82
|
+
}
|
|
83
|
+
if ("rollback" in event) {
|
|
84
|
+
return "rollback";
|
|
85
|
+
}
|
|
86
|
+
return "workflow";
|
|
87
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -115,6 +115,7 @@ export class StepFailedError extends Error {
|
|
|
115
115
|
public readonly stepName: string,
|
|
116
116
|
public readonly originalError: unknown,
|
|
117
117
|
public readonly attempts: number,
|
|
118
|
+
public readonly retryAt: number,
|
|
118
119
|
) {
|
|
119
120
|
super(`Step "${stepName}" failed (attempt ${attempts})`);
|
|
120
121
|
this.name = "StepFailedError";
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,7 @@ import type { Logger } from "pino";
|
|
|
4
4
|
|
|
5
5
|
// Context
|
|
6
6
|
export {
|
|
7
|
-
|
|
8
|
-
DEFAULT_LOOP_HISTORY_EVERY,
|
|
9
|
-
DEFAULT_LOOP_HISTORY_KEEP,
|
|
7
|
+
DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL,
|
|
10
8
|
DEFAULT_MAX_RETRIES,
|
|
11
9
|
DEFAULT_RETRY_BACKOFF_BASE,
|
|
12
10
|
DEFAULT_RETRY_BACKOFF_MAX,
|
|
@@ -31,6 +29,7 @@ export {
|
|
|
31
29
|
StepExhaustedError,
|
|
32
30
|
StepFailedError,
|
|
33
31
|
} from "./errors.js";
|
|
32
|
+
export { extractErrorInfo } from "./error-utils.js";
|
|
34
33
|
|
|
35
34
|
// Location utilities
|
|
36
35
|
export {
|
|
@@ -95,8 +94,12 @@ export type {
|
|
|
95
94
|
StepEntry,
|
|
96
95
|
Storage,
|
|
97
96
|
WorkflowContextInterface,
|
|
97
|
+
WorkflowError,
|
|
98
|
+
WorkflowErrorEvent,
|
|
99
|
+
WorkflowErrorHandler,
|
|
98
100
|
WorkflowFunction,
|
|
99
101
|
WorkflowHandle,
|
|
102
|
+
WorkflowRollbackErrorEvent,
|
|
100
103
|
WorkflowQueue,
|
|
101
104
|
WorkflowQueueMessage,
|
|
102
105
|
WorkflowQueueNextBatchOptions,
|
|
@@ -104,7 +107,9 @@ export type {
|
|
|
104
107
|
WorkflowMessageDriver,
|
|
105
108
|
WorkflowResult,
|
|
106
109
|
WorkflowRunMode,
|
|
110
|
+
WorkflowRunErrorEvent,
|
|
107
111
|
WorkflowState,
|
|
112
|
+
WorkflowStepErrorEvent,
|
|
108
113
|
} from "./types.js";
|
|
109
114
|
|
|
110
115
|
// Loop result helpers
|
|
@@ -132,20 +137,32 @@ import { type RollbackAction, WorkflowContextImpl } from "./context.js";
|
|
|
132
137
|
// Main workflow runner
|
|
133
138
|
import type { EngineDriver } from "./driver.js";
|
|
134
139
|
import {
|
|
140
|
+
extractErrorInfo,
|
|
141
|
+
getErrorEventTag,
|
|
142
|
+
isErrorReported,
|
|
143
|
+
markErrorReported,
|
|
144
|
+
} from "./error-utils.js";
|
|
145
|
+
import {
|
|
146
|
+
CriticalError,
|
|
135
147
|
EvictedError,
|
|
136
148
|
MessageWaitError,
|
|
137
149
|
RollbackCheckpointError,
|
|
150
|
+
RollbackError,
|
|
138
151
|
RollbackStopError,
|
|
139
152
|
SleepError,
|
|
153
|
+
StepExhaustedError,
|
|
140
154
|
StepFailedError,
|
|
141
155
|
} from "./errors.js";
|
|
142
156
|
import {
|
|
157
|
+
buildEntryMetadataKey,
|
|
143
158
|
buildEntryMetadataPrefix,
|
|
159
|
+
buildHistoryKey,
|
|
144
160
|
buildWorkflowErrorKey,
|
|
145
161
|
buildWorkflowInputKey,
|
|
146
162
|
buildWorkflowOutputKey,
|
|
147
163
|
buildWorkflowStateKey,
|
|
148
164
|
} from "./keys.js";
|
|
165
|
+
import { isLocationPrefix } from "./location.js";
|
|
149
166
|
import {
|
|
150
167
|
createHistorySnapshot,
|
|
151
168
|
flush,
|
|
@@ -154,9 +171,12 @@ import {
|
|
|
154
171
|
loadStorage,
|
|
155
172
|
} from "./storage.js";
|
|
156
173
|
import type {
|
|
174
|
+
Entry,
|
|
175
|
+
EntryMetadata,
|
|
157
176
|
RollbackContextInterface,
|
|
158
177
|
RunWorkflowOptions,
|
|
159
178
|
Storage,
|
|
179
|
+
WorkflowErrorEvent,
|
|
160
180
|
WorkflowHistorySnapshot,
|
|
161
181
|
WorkflowFunction,
|
|
162
182
|
WorkflowHandle,
|
|
@@ -184,6 +204,12 @@ interface LiveRuntime {
|
|
|
184
204
|
|
|
185
205
|
type HistoryNotifier = (() => void) | undefined;
|
|
186
206
|
|
|
207
|
+
type ReplayEntryRecord = {
|
|
208
|
+
key: string;
|
|
209
|
+
entry: Entry;
|
|
210
|
+
metadata: EntryMetadata;
|
|
211
|
+
};
|
|
212
|
+
|
|
187
213
|
function createLiveRuntime(): LiveRuntime {
|
|
188
214
|
return {
|
|
189
215
|
isSleeping: false,
|
|
@@ -252,6 +278,7 @@ async function executeRollback<TInput, TOutput>(
|
|
|
252
278
|
abortController: AbortController,
|
|
253
279
|
storage: Storage,
|
|
254
280
|
historyNotifier?: HistoryNotifier,
|
|
281
|
+
onError?: RunWorkflowOptions["onError"],
|
|
255
282
|
logger?: Logger,
|
|
256
283
|
): Promise<void> {
|
|
257
284
|
const rollbackActions: RollbackAction[] = [];
|
|
@@ -266,6 +293,7 @@ async function executeRollback<TInput, TOutput>(
|
|
|
266
293
|
rollbackActions,
|
|
267
294
|
false,
|
|
268
295
|
historyNotifier,
|
|
296
|
+
onError,
|
|
269
297
|
logger,
|
|
270
298
|
);
|
|
271
299
|
|
|
@@ -299,6 +327,7 @@ async function executeRollback<TInput, TOutput>(
|
|
|
299
327
|
continue;
|
|
300
328
|
}
|
|
301
329
|
|
|
330
|
+
let rollbackEvent: WorkflowErrorEvent | undefined;
|
|
302
331
|
try {
|
|
303
332
|
await awaitWithEviction(
|
|
304
333
|
action.rollback(rollbackContext, action.output),
|
|
@@ -312,14 +341,45 @@ async function executeRollback<TInput, TOutput>(
|
|
|
312
341
|
}
|
|
313
342
|
metadata.rollbackError =
|
|
314
343
|
error instanceof Error ? error.message : String(error);
|
|
344
|
+
if (onError) {
|
|
345
|
+
rollbackEvent = {
|
|
346
|
+
rollback: {
|
|
347
|
+
workflowId,
|
|
348
|
+
stepName: action.name,
|
|
349
|
+
error: extractErrorInfo(error),
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (error instanceof Error) {
|
|
354
|
+
markErrorReported(error);
|
|
355
|
+
}
|
|
315
356
|
throw error;
|
|
316
357
|
} finally {
|
|
317
358
|
metadata.dirty = true;
|
|
318
359
|
await flush(storage, driver, historyNotifier);
|
|
360
|
+
if (rollbackEvent && onError) {
|
|
361
|
+
await notifyError(onError, logger, rollbackEvent);
|
|
362
|
+
}
|
|
319
363
|
}
|
|
320
364
|
}
|
|
321
365
|
}
|
|
322
366
|
|
|
367
|
+
async function notifyError(
|
|
368
|
+
onError: NonNullable<RunWorkflowOptions["onError"]>,
|
|
369
|
+
logger: Logger | undefined,
|
|
370
|
+
event: WorkflowErrorEvent,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
try {
|
|
373
|
+
await onError(event);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
logger?.warn({
|
|
376
|
+
msg: "workflow error hook failed",
|
|
377
|
+
hookEventType: getErrorEventTag(event),
|
|
378
|
+
error: extractErrorInfo(error),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
323
383
|
async function setSleepState<TOutput>(
|
|
324
384
|
storage: Storage,
|
|
325
385
|
driver: EngineDriver,
|
|
@@ -364,12 +424,12 @@ async function setRetryState<TOutput>(
|
|
|
364
424
|
storage: Storage,
|
|
365
425
|
driver: EngineDriver,
|
|
366
426
|
workflowId: string,
|
|
427
|
+
retryAt: number,
|
|
367
428
|
historyNotifier?: HistoryNotifier,
|
|
368
429
|
): Promise<WorkflowResult<TOutput>> {
|
|
369
430
|
storage.state = "sleeping";
|
|
370
431
|
await flush(storage, driver, historyNotifier);
|
|
371
432
|
|
|
372
|
-
const retryAt = Date.now() + 100;
|
|
373
433
|
await driver.setAlarm(workflowId, retryAt);
|
|
374
434
|
|
|
375
435
|
return { state: "sleeping", sleepUntil: retryAt };
|
|
@@ -437,6 +497,7 @@ async function executeLiveWorkflow<TInput, TOutput>(
|
|
|
437
497
|
abortController: AbortController,
|
|
438
498
|
runtime: LiveRuntime,
|
|
439
499
|
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
|
|
500
|
+
onError?: RunWorkflowOptions["onError"],
|
|
440
501
|
logger?: Logger,
|
|
441
502
|
): Promise<WorkflowResult<TOutput>> {
|
|
442
503
|
let lastResult: WorkflowResult<TOutput> | undefined;
|
|
@@ -450,6 +511,7 @@ async function executeLiveWorkflow<TInput, TOutput>(
|
|
|
450
511
|
messageDriver,
|
|
451
512
|
abortController,
|
|
452
513
|
onHistoryUpdated,
|
|
514
|
+
onError,
|
|
453
515
|
logger,
|
|
454
516
|
);
|
|
455
517
|
lastResult = result;
|
|
@@ -549,6 +611,7 @@ export function runWorkflow<TInput, TOutput>(
|
|
|
549
611
|
abortController,
|
|
550
612
|
liveRuntime,
|
|
551
613
|
options.onHistoryUpdated,
|
|
614
|
+
options.onError,
|
|
552
615
|
logger,
|
|
553
616
|
)
|
|
554
617
|
: executeWorkflow(
|
|
@@ -559,6 +622,7 @@ export function runWorkflow<TInput, TOutput>(
|
|
|
559
622
|
messageDriver,
|
|
560
623
|
abortController,
|
|
561
624
|
options.onHistoryUpdated,
|
|
625
|
+
options.onError,
|
|
562
626
|
logger,
|
|
563
627
|
);
|
|
564
628
|
|
|
@@ -675,6 +739,125 @@ export function runWorkflow<TInput, TOutput>(
|
|
|
675
739
|
};
|
|
676
740
|
}
|
|
677
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Remove a step and every later workflow entry, then schedule the workflow to
|
|
744
|
+
* start again immediately. Omitting `entryId` replays the workflow from the
|
|
745
|
+
* beginning.
|
|
746
|
+
*/
|
|
747
|
+
export async function replayWorkflowFromStep(
|
|
748
|
+
workflowId: string,
|
|
749
|
+
driver: EngineDriver,
|
|
750
|
+
entryId?: string,
|
|
751
|
+
options?: {
|
|
752
|
+
scheduleAlarm?: boolean;
|
|
753
|
+
},
|
|
754
|
+
): Promise<WorkflowHistorySnapshot> {
|
|
755
|
+
const storage = await loadStorage(driver);
|
|
756
|
+
const entries = await Promise.all(
|
|
757
|
+
Array.from(storage.history.entries.entries()).map(
|
|
758
|
+
async ([key, entry]) => ({
|
|
759
|
+
key,
|
|
760
|
+
entry,
|
|
761
|
+
metadata: await loadMetadata(storage, driver, entry.id),
|
|
762
|
+
}),
|
|
763
|
+
),
|
|
764
|
+
);
|
|
765
|
+
const ordered = [...entries].sort((a, b) => {
|
|
766
|
+
if (a.metadata.createdAt !== b.metadata.createdAt) {
|
|
767
|
+
return a.metadata.createdAt - b.metadata.createdAt;
|
|
768
|
+
}
|
|
769
|
+
return a.key.localeCompare(b.key);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
let entriesToDelete = ordered;
|
|
773
|
+
if (entryId !== undefined) {
|
|
774
|
+
const target = entries.find(({ entry }) => entry.id === entryId);
|
|
775
|
+
if (!target) {
|
|
776
|
+
throw new Error(`Workflow step not found: ${entryId}`);
|
|
777
|
+
}
|
|
778
|
+
if (target.entry.kind.type !== "step") {
|
|
779
|
+
throw new Error("Workflow replay target must be a step");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const replayBoundary = findReplayBoundaryEntry(entries, target);
|
|
783
|
+
const targetIndex = ordered.findIndex(
|
|
784
|
+
({ entry }) => entry.id === replayBoundary.entry.id,
|
|
785
|
+
);
|
|
786
|
+
entriesToDelete = ordered.slice(targetIndex);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const entryIdsToDelete = new Set(
|
|
790
|
+
entriesToDelete.map(({ entry }) => entry.id),
|
|
791
|
+
);
|
|
792
|
+
if (
|
|
793
|
+
entries.some(
|
|
794
|
+
({ entry, metadata }) =>
|
|
795
|
+
metadata.status === "running" &&
|
|
796
|
+
!entryIdsToDelete.has(entry.id),
|
|
797
|
+
)
|
|
798
|
+
) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
"Cannot replay a workflow while a step is currently running",
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
await Promise.all(
|
|
805
|
+
entriesToDelete.flatMap(({ entry }) => [
|
|
806
|
+
driver.delete(buildHistoryKey(entry.location)),
|
|
807
|
+
driver.delete(buildEntryMetadataKey(entry.id)),
|
|
808
|
+
]),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
for (const { key, entry } of entriesToDelete) {
|
|
812
|
+
storage.history.entries.delete(key);
|
|
813
|
+
storage.entryMetadata.delete(entry.id);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
storage.output = undefined;
|
|
817
|
+
storage.flushedOutput = undefined;
|
|
818
|
+
storage.error = undefined;
|
|
819
|
+
storage.flushedError = undefined;
|
|
820
|
+
storage.state = "sleeping";
|
|
821
|
+
storage.flushedState = "sleeping";
|
|
822
|
+
|
|
823
|
+
await Promise.all([
|
|
824
|
+
driver.delete(buildWorkflowOutputKey()),
|
|
825
|
+
driver.delete(buildWorkflowErrorKey()),
|
|
826
|
+
driver.set(buildWorkflowStateKey(), serializeWorkflowState("sleeping")),
|
|
827
|
+
]);
|
|
828
|
+
if (options?.scheduleAlarm ?? true) {
|
|
829
|
+
await driver.setAlarm(workflowId, Date.now());
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return createHistorySnapshot(storage);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function findReplayBoundaryEntry(
|
|
836
|
+
entries: ReplayEntryRecord[],
|
|
837
|
+
target: ReplayEntryRecord,
|
|
838
|
+
): ReplayEntryRecord {
|
|
839
|
+
let boundary = target;
|
|
840
|
+
let boundaryDepth = -1;
|
|
841
|
+
|
|
842
|
+
for (const candidate of entries) {
|
|
843
|
+
if (candidate.entry.kind.type !== "loop") {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (
|
|
847
|
+
candidate.entry.location.length >= target.entry.location.length ||
|
|
848
|
+
!isLocationPrefix(candidate.entry.location, target.entry.location)
|
|
849
|
+
) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (candidate.entry.location.length > boundaryDepth) {
|
|
853
|
+
boundary = candidate;
|
|
854
|
+
boundaryDepth = candidate.entry.location.length;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return boundary;
|
|
859
|
+
}
|
|
860
|
+
|
|
678
861
|
/**
|
|
679
862
|
* Internal: Execute the workflow and return the result.
|
|
680
863
|
*/
|
|
@@ -686,6 +869,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
686
869
|
messageDriver: WorkflowMessageDriver,
|
|
687
870
|
abortController: AbortController,
|
|
688
871
|
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
|
|
872
|
+
onError?: RunWorkflowOptions["onError"],
|
|
689
873
|
logger?: Logger,
|
|
690
874
|
): Promise<WorkflowResult<TOutput>> {
|
|
691
875
|
const storage = await loadStorage(driver);
|
|
@@ -739,6 +923,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
739
923
|
abortController,
|
|
740
924
|
storage,
|
|
741
925
|
historyNotifier,
|
|
926
|
+
onError,
|
|
742
927
|
logger,
|
|
743
928
|
);
|
|
744
929
|
} catch (error) {
|
|
@@ -771,6 +956,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
771
956
|
undefined,
|
|
772
957
|
false,
|
|
773
958
|
historyNotifier,
|
|
959
|
+
onError,
|
|
774
960
|
logger,
|
|
775
961
|
);
|
|
776
962
|
|
|
@@ -815,12 +1001,21 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
815
1001
|
storage,
|
|
816
1002
|
driver,
|
|
817
1003
|
workflowId,
|
|
1004
|
+
error.retryAt,
|
|
818
1005
|
historyNotifier,
|
|
819
1006
|
);
|
|
820
1007
|
}
|
|
821
1008
|
|
|
822
1009
|
if (error instanceof RollbackCheckpointError) {
|
|
823
1010
|
await setFailedState(storage, driver, error, historyNotifier);
|
|
1011
|
+
if (onError && !isErrorReported(error)) {
|
|
1012
|
+
await notifyError(onError, logger, {
|
|
1013
|
+
workflow: {
|
|
1014
|
+
workflowId,
|
|
1015
|
+
error: extractErrorInfo(error),
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
824
1019
|
throw error;
|
|
825
1020
|
}
|
|
826
1021
|
|
|
@@ -839,6 +1034,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
839
1034
|
abortController,
|
|
840
1035
|
storage,
|
|
841
1036
|
historyNotifier,
|
|
1037
|
+
onError,
|
|
842
1038
|
logger,
|
|
843
1039
|
);
|
|
844
1040
|
} catch (rollbackError) {
|
|
@@ -850,59 +1046,22 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
850
1046
|
|
|
851
1047
|
storage.state = "failed";
|
|
852
1048
|
await flush(storage, driver, historyNotifier);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} {
|
|
867
|
-
if (error instanceof Error) {
|
|
868
|
-
const result: {
|
|
869
|
-
name: string;
|
|
870
|
-
message: string;
|
|
871
|
-
stack?: string;
|
|
872
|
-
metadata?: Record<string, unknown>;
|
|
873
|
-
} = {
|
|
874
|
-
name: error.name,
|
|
875
|
-
message: error.message,
|
|
876
|
-
stack: error.stack,
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
// Extract custom properties from error
|
|
880
|
-
const metadata: Record<string, unknown> = {};
|
|
881
|
-
for (const key of Object.keys(error)) {
|
|
882
|
-
if (key !== "name" && key !== "message" && key !== "stack") {
|
|
883
|
-
const value = (error as unknown as Record<string, unknown>)[
|
|
884
|
-
key
|
|
885
|
-
];
|
|
886
|
-
// Only include serializable values
|
|
887
|
-
if (
|
|
888
|
-
typeof value === "string" ||
|
|
889
|
-
typeof value === "number" ||
|
|
890
|
-
typeof value === "boolean" ||
|
|
891
|
-
value === null
|
|
892
|
-
) {
|
|
893
|
-
metadata[key] = value;
|
|
894
|
-
}
|
|
1049
|
+
if (onError && !isErrorReported(error)) {
|
|
1050
|
+
await notifyError(onError, logger, {
|
|
1051
|
+
workflow: {
|
|
1052
|
+
workflowId,
|
|
1053
|
+
error: extractErrorInfo(error),
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
if (
|
|
1057
|
+
error instanceof CriticalError ||
|
|
1058
|
+
error instanceof RollbackError ||
|
|
1059
|
+
error instanceof StepExhaustedError
|
|
1060
|
+
) {
|
|
1061
|
+
markErrorReported(error);
|
|
895
1062
|
}
|
|
896
1063
|
}
|
|
897
|
-
if (Object.keys(metadata).length > 0) {
|
|
898
|
-
result.metadata = metadata;
|
|
899
|
-
}
|
|
900
1064
|
|
|
901
|
-
|
|
1065
|
+
throw error;
|
|
902
1066
|
}
|
|
903
|
-
|
|
904
|
-
return {
|
|
905
|
-
name: "Error",
|
|
906
|
-
message: String(error),
|
|
907
|
-
};
|
|
908
1067
|
}
|
package/src/keys.ts
CHANGED
|
@@ -142,6 +142,32 @@ export function buildHistoryPrefix(location: Location): Uint8Array {
|
|
|
142
142
|
return pack([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Build a key range for loop iteration history entries.
|
|
147
|
+
* Range: [2, ...locationSegments, [loopSegment, fromIteration]] to
|
|
148
|
+
* [2, ...locationSegments, [loopSegment, toIteration]])
|
|
149
|
+
*/
|
|
150
|
+
export function buildLoopIterationRange(
|
|
151
|
+
loopLocation: Location,
|
|
152
|
+
loopSegment: number,
|
|
153
|
+
fromIteration: number,
|
|
154
|
+
toIteration: number,
|
|
155
|
+
): { start: Uint8Array; end: Uint8Array } {
|
|
156
|
+
const loopLocationSegments = locationToTupleElements(loopLocation);
|
|
157
|
+
return {
|
|
158
|
+
start: pack([
|
|
159
|
+
KEY_PREFIX.HISTORY,
|
|
160
|
+
...loopLocationSegments,
|
|
161
|
+
[loopSegment, fromIteration],
|
|
162
|
+
]),
|
|
163
|
+
end: pack([
|
|
164
|
+
KEY_PREFIX.HISTORY,
|
|
165
|
+
...loopLocationSegments,
|
|
166
|
+
[loopSegment, toIteration],
|
|
167
|
+
]),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
/**
|
|
146
172
|
* Build a prefix for listing all history entries.
|
|
147
173
|
* Prefix: [2]
|
package/src/location.ts
CHANGED
|
@@ -107,7 +107,10 @@ export function isLocationPrefix(
|
|
|
107
107
|
const prefixSegment = prefix[i];
|
|
108
108
|
const locationSegment = location[i];
|
|
109
109
|
|
|
110
|
-
if (
|
|
110
|
+
if (
|
|
111
|
+
typeof prefixSegment === "number" &&
|
|
112
|
+
typeof locationSegment === "number"
|
|
113
|
+
) {
|
|
111
114
|
if (prefixSegment !== locationSegment) {
|
|
112
115
|
return false;
|
|
113
116
|
}
|