@rivetkit/workflow-engine 2.1.5 → 2.1.6-rc.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-JTLDEP6X.js → chunk-MMWB37UG.js} +449 -209
- package/dist/tsup/chunk-MMWB37UG.js.map +1 -0
- package/dist/tsup/{chunk-KQO2TD7T.cjs → chunk-SHICGAKC.cjs} +448 -208
- package/dist/tsup/chunk-SHICGAKC.cjs.map +1 -0
- package/dist/tsup/index.cjs +2 -4
- package/dist/tsup/index.cjs.map +1 -1
- package/dist/tsup/index.d.cts +85 -25
- package/dist/tsup/index.d.ts +85 -25
- package/dist/tsup/index.js +5 -7
- package/dist/tsup/testing.cjs +35 -25
- 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 +19 -9
- package/dist/tsup/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/context.ts +298 -114
- package/src/driver.ts +5 -0
- package/src/error-utils.ts +87 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +84 -55
- package/src/keys.ts +26 -0
- package/src/location.ts +4 -1
- package/src/storage.ts +62 -21
- 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,11 +137,20 @@ 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,6 +171,7 @@ import type {
|
|
|
157
171
|
RollbackContextInterface,
|
|
158
172
|
RunWorkflowOptions,
|
|
159
173
|
Storage,
|
|
174
|
+
WorkflowErrorEvent,
|
|
160
175
|
WorkflowHistorySnapshot,
|
|
161
176
|
WorkflowFunction,
|
|
162
177
|
WorkflowHandle,
|
|
@@ -252,6 +267,7 @@ async function executeRollback<TInput, TOutput>(
|
|
|
252
267
|
abortController: AbortController,
|
|
253
268
|
storage: Storage,
|
|
254
269
|
historyNotifier?: HistoryNotifier,
|
|
270
|
+
onError?: RunWorkflowOptions["onError"],
|
|
255
271
|
logger?: Logger,
|
|
256
272
|
): Promise<void> {
|
|
257
273
|
const rollbackActions: RollbackAction[] = [];
|
|
@@ -266,6 +282,7 @@ async function executeRollback<TInput, TOutput>(
|
|
|
266
282
|
rollbackActions,
|
|
267
283
|
false,
|
|
268
284
|
historyNotifier,
|
|
285
|
+
onError,
|
|
269
286
|
logger,
|
|
270
287
|
);
|
|
271
288
|
|
|
@@ -299,6 +316,7 @@ async function executeRollback<TInput, TOutput>(
|
|
|
299
316
|
continue;
|
|
300
317
|
}
|
|
301
318
|
|
|
319
|
+
let rollbackEvent: WorkflowErrorEvent | undefined;
|
|
302
320
|
try {
|
|
303
321
|
await awaitWithEviction(
|
|
304
322
|
action.rollback(rollbackContext, action.output),
|
|
@@ -312,14 +330,45 @@ async function executeRollback<TInput, TOutput>(
|
|
|
312
330
|
}
|
|
313
331
|
metadata.rollbackError =
|
|
314
332
|
error instanceof Error ? error.message : String(error);
|
|
333
|
+
if (onError) {
|
|
334
|
+
rollbackEvent = {
|
|
335
|
+
rollback: {
|
|
336
|
+
workflowId,
|
|
337
|
+
stepName: action.name,
|
|
338
|
+
error: extractErrorInfo(error),
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (error instanceof Error) {
|
|
343
|
+
markErrorReported(error);
|
|
344
|
+
}
|
|
315
345
|
throw error;
|
|
316
346
|
} finally {
|
|
317
347
|
metadata.dirty = true;
|
|
318
348
|
await flush(storage, driver, historyNotifier);
|
|
349
|
+
if (rollbackEvent && onError) {
|
|
350
|
+
await notifyError(onError, logger, rollbackEvent);
|
|
351
|
+
}
|
|
319
352
|
}
|
|
320
353
|
}
|
|
321
354
|
}
|
|
322
355
|
|
|
356
|
+
async function notifyError(
|
|
357
|
+
onError: NonNullable<RunWorkflowOptions["onError"]>,
|
|
358
|
+
logger: Logger | undefined,
|
|
359
|
+
event: WorkflowErrorEvent,
|
|
360
|
+
): Promise<void> {
|
|
361
|
+
try {
|
|
362
|
+
await onError(event);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
logger?.warn({
|
|
365
|
+
msg: "workflow error hook failed",
|
|
366
|
+
hookEventType: getErrorEventTag(event),
|
|
367
|
+
error: extractErrorInfo(error),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
323
372
|
async function setSleepState<TOutput>(
|
|
324
373
|
storage: Storage,
|
|
325
374
|
driver: EngineDriver,
|
|
@@ -364,12 +413,12 @@ async function setRetryState<TOutput>(
|
|
|
364
413
|
storage: Storage,
|
|
365
414
|
driver: EngineDriver,
|
|
366
415
|
workflowId: string,
|
|
416
|
+
retryAt: number,
|
|
367
417
|
historyNotifier?: HistoryNotifier,
|
|
368
418
|
): Promise<WorkflowResult<TOutput>> {
|
|
369
419
|
storage.state = "sleeping";
|
|
370
420
|
await flush(storage, driver, historyNotifier);
|
|
371
421
|
|
|
372
|
-
const retryAt = Date.now() + 100;
|
|
373
422
|
await driver.setAlarm(workflowId, retryAt);
|
|
374
423
|
|
|
375
424
|
return { state: "sleeping", sleepUntil: retryAt };
|
|
@@ -437,6 +486,7 @@ async function executeLiveWorkflow<TInput, TOutput>(
|
|
|
437
486
|
abortController: AbortController,
|
|
438
487
|
runtime: LiveRuntime,
|
|
439
488
|
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
|
|
489
|
+
onError?: RunWorkflowOptions["onError"],
|
|
440
490
|
logger?: Logger,
|
|
441
491
|
): Promise<WorkflowResult<TOutput>> {
|
|
442
492
|
let lastResult: WorkflowResult<TOutput> | undefined;
|
|
@@ -450,6 +500,7 @@ async function executeLiveWorkflow<TInput, TOutput>(
|
|
|
450
500
|
messageDriver,
|
|
451
501
|
abortController,
|
|
452
502
|
onHistoryUpdated,
|
|
503
|
+
onError,
|
|
453
504
|
logger,
|
|
454
505
|
);
|
|
455
506
|
lastResult = result;
|
|
@@ -549,6 +600,7 @@ export function runWorkflow<TInput, TOutput>(
|
|
|
549
600
|
abortController,
|
|
550
601
|
liveRuntime,
|
|
551
602
|
options.onHistoryUpdated,
|
|
603
|
+
options.onError,
|
|
552
604
|
logger,
|
|
553
605
|
)
|
|
554
606
|
: executeWorkflow(
|
|
@@ -559,6 +611,7 @@ export function runWorkflow<TInput, TOutput>(
|
|
|
559
611
|
messageDriver,
|
|
560
612
|
abortController,
|
|
561
613
|
options.onHistoryUpdated,
|
|
614
|
+
options.onError,
|
|
562
615
|
logger,
|
|
563
616
|
);
|
|
564
617
|
|
|
@@ -686,6 +739,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
686
739
|
messageDriver: WorkflowMessageDriver,
|
|
687
740
|
abortController: AbortController,
|
|
688
741
|
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
|
|
742
|
+
onError?: RunWorkflowOptions["onError"],
|
|
689
743
|
logger?: Logger,
|
|
690
744
|
): Promise<WorkflowResult<TOutput>> {
|
|
691
745
|
const storage = await loadStorage(driver);
|
|
@@ -739,6 +793,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
739
793
|
abortController,
|
|
740
794
|
storage,
|
|
741
795
|
historyNotifier,
|
|
796
|
+
onError,
|
|
742
797
|
logger,
|
|
743
798
|
);
|
|
744
799
|
} catch (error) {
|
|
@@ -771,6 +826,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
771
826
|
undefined,
|
|
772
827
|
false,
|
|
773
828
|
historyNotifier,
|
|
829
|
+
onError,
|
|
774
830
|
logger,
|
|
775
831
|
);
|
|
776
832
|
|
|
@@ -815,12 +871,21 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
815
871
|
storage,
|
|
816
872
|
driver,
|
|
817
873
|
workflowId,
|
|
874
|
+
error.retryAt,
|
|
818
875
|
historyNotifier,
|
|
819
876
|
);
|
|
820
877
|
}
|
|
821
878
|
|
|
822
879
|
if (error instanceof RollbackCheckpointError) {
|
|
823
880
|
await setFailedState(storage, driver, error, historyNotifier);
|
|
881
|
+
if (onError && !isErrorReported(error)) {
|
|
882
|
+
await notifyError(onError, logger, {
|
|
883
|
+
workflow: {
|
|
884
|
+
workflowId,
|
|
885
|
+
error: extractErrorInfo(error),
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
}
|
|
824
889
|
throw error;
|
|
825
890
|
}
|
|
826
891
|
|
|
@@ -839,6 +904,7 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
839
904
|
abortController,
|
|
840
905
|
storage,
|
|
841
906
|
historyNotifier,
|
|
907
|
+
onError,
|
|
842
908
|
logger,
|
|
843
909
|
);
|
|
844
910
|
} catch (rollbackError) {
|
|
@@ -850,59 +916,22 @@ async function executeWorkflow<TInput, TOutput>(
|
|
|
850
916
|
|
|
851
917
|
storage.state = "failed";
|
|
852
918
|
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
|
-
}
|
|
919
|
+
if (onError && !isErrorReported(error)) {
|
|
920
|
+
await notifyError(onError, logger, {
|
|
921
|
+
workflow: {
|
|
922
|
+
workflowId,
|
|
923
|
+
error: extractErrorInfo(error),
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
if (
|
|
927
|
+
error instanceof CriticalError ||
|
|
928
|
+
error instanceof RollbackError ||
|
|
929
|
+
error instanceof StepExhaustedError
|
|
930
|
+
) {
|
|
931
|
+
markErrorReported(error);
|
|
895
932
|
}
|
|
896
933
|
}
|
|
897
|
-
if (Object.keys(metadata).length > 0) {
|
|
898
|
-
result.metadata = metadata;
|
|
899
|
-
}
|
|
900
934
|
|
|
901
|
-
|
|
935
|
+
throw error;
|
|
902
936
|
}
|
|
903
|
-
|
|
904
|
-
return {
|
|
905
|
-
name: "Error",
|
|
906
|
-
message: String(error),
|
|
907
|
-
};
|
|
908
937
|
}
|
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
|
}
|
package/src/storage.ts
CHANGED
|
@@ -129,9 +129,7 @@ export function getOrCreateMetadata(
|
|
|
129
129
|
/**
|
|
130
130
|
* Load storage from the driver.
|
|
131
131
|
*/
|
|
132
|
-
export async function loadStorage(
|
|
133
|
-
driver: EngineDriver,
|
|
134
|
-
): Promise<Storage> {
|
|
132
|
+
export async function loadStorage(driver: EngineDriver): Promise<Storage> {
|
|
135
133
|
const storage = createStorage();
|
|
136
134
|
|
|
137
135
|
// Load name registry
|
|
@@ -207,12 +205,25 @@ export async function loadMetadata(
|
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
/**
|
|
210
|
-
*
|
|
208
|
+
* Pending deletions collected by collectLoopPruning to be included
|
|
209
|
+
* in the next flush alongside the state write.
|
|
210
|
+
*/
|
|
211
|
+
export interface PendingDeletions {
|
|
212
|
+
prefixes: Uint8Array[];
|
|
213
|
+
keys: Uint8Array[];
|
|
214
|
+
ranges: { start: Uint8Array; end: Uint8Array }[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Flush all dirty data to the driver. Optionally includes pending
|
|
219
|
+
* deletions so that history pruning happens alongside the
|
|
220
|
+
* state write.
|
|
211
221
|
*/
|
|
212
222
|
export async function flush(
|
|
213
223
|
storage: Storage,
|
|
214
224
|
driver: EngineDriver,
|
|
215
225
|
onHistoryUpdated?: () => void,
|
|
226
|
+
pendingDeletions?: PendingDeletions,
|
|
216
227
|
): Promise<void> {
|
|
217
228
|
const writes: KVWrite[] = [];
|
|
218
229
|
let historyUpdated = false;
|
|
@@ -293,6 +304,25 @@ export async function flush(
|
|
|
293
304
|
await driver.batch(writes);
|
|
294
305
|
}
|
|
295
306
|
|
|
307
|
+
// Apply pending deletions after the batch write. These are collected
|
|
308
|
+
// by collectLoopPruning so pruning happens alongside the state write.
|
|
309
|
+
if (pendingDeletions) {
|
|
310
|
+
const deleteOps: Promise<void>[] = [];
|
|
311
|
+
for (const prefix of pendingDeletions.prefixes) {
|
|
312
|
+
deleteOps.push(driver.deletePrefix(prefix));
|
|
313
|
+
}
|
|
314
|
+
for (const range of pendingDeletions.ranges) {
|
|
315
|
+
deleteOps.push(driver.deleteRange(range.start, range.end));
|
|
316
|
+
}
|
|
317
|
+
for (const key of pendingDeletions.keys) {
|
|
318
|
+
deleteOps.push(driver.delete(key));
|
|
319
|
+
}
|
|
320
|
+
if (deleteOps.length > 0) {
|
|
321
|
+
await Promise.all(deleteOps);
|
|
322
|
+
historyUpdated = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
296
326
|
// Update flushed tracking after successful write
|
|
297
327
|
storage.flushedNameCount = storage.nameRegistry.length;
|
|
298
328
|
storage.flushedState = storage.state;
|
|
@@ -314,30 +344,41 @@ export async function deleteEntriesWithPrefix(
|
|
|
314
344
|
prefixLocation: Location,
|
|
315
345
|
onHistoryUpdated?: () => void,
|
|
316
346
|
): Promise<void> {
|
|
317
|
-
|
|
318
|
-
|
|
347
|
+
const deletions = collectDeletionsForPrefix(storage, prefixLocation);
|
|
348
|
+
|
|
349
|
+
// Apply deletions to driver
|
|
350
|
+
await driver.deletePrefix(deletions.prefixes[0]!);
|
|
351
|
+
await Promise.all(deletions.keys.map((key) => driver.delete(key)));
|
|
352
|
+
|
|
353
|
+
if (deletions.keys.length > 0 && onHistoryUpdated) {
|
|
354
|
+
onHistoryUpdated();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Remove entries matching a location prefix from memory and collect
|
|
360
|
+
* the driver-level deletion operations. The returned PendingDeletions
|
|
361
|
+
* can be applied immediately or batched with a flush.
|
|
362
|
+
*/
|
|
363
|
+
export function collectDeletionsForPrefix(
|
|
364
|
+
storage: Storage,
|
|
365
|
+
prefixLocation: Location,
|
|
366
|
+
): PendingDeletions {
|
|
367
|
+
const pending: PendingDeletions = {
|
|
368
|
+
prefixes: [buildHistoryPrefix(prefixLocation)],
|
|
369
|
+
keys: [],
|
|
370
|
+
ranges: [],
|
|
371
|
+
};
|
|
319
372
|
|
|
320
|
-
// Collect entries to delete and their IDs
|
|
321
373
|
for (const [key, entry] of storage.history.entries) {
|
|
322
|
-
// Check if the entry's location starts with the prefix location
|
|
323
374
|
if (isLocationPrefix(prefixLocation, entry.location)) {
|
|
324
|
-
|
|
375
|
+
pending.keys.push(buildEntryMetadataKey(entry.id));
|
|
325
376
|
storage.entryMetadata.delete(entry.id);
|
|
326
377
|
storage.history.entries.delete(key);
|
|
327
378
|
}
|
|
328
379
|
}
|
|
329
380
|
|
|
330
|
-
|
|
331
|
-
await driver.deletePrefix(buildHistoryPrefix(prefixLocation));
|
|
332
|
-
|
|
333
|
-
// Delete metadata from driver in parallel
|
|
334
|
-
await Promise.all(
|
|
335
|
-
entryIds.map((id) => driver.delete(buildEntryMetadataKey(id))),
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
if (entryIds.length > 0 && onHistoryUpdated) {
|
|
339
|
-
onHistoryUpdated();
|
|
340
|
-
}
|
|
381
|
+
return pending;
|
|
341
382
|
}
|
|
342
383
|
|
|
343
384
|
/**
|
|
@@ -361,4 +402,4 @@ export function setEntry(
|
|
|
361
402
|
): void {
|
|
362
403
|
const key = locationToKey(storage, location);
|
|
363
404
|
storage.history.entries.set(key, entry);
|
|
364
|
-
}
|
|
405
|
+
}
|
package/src/testing.ts
CHANGED
|
@@ -33,7 +33,11 @@ class InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {
|
|
|
33
33
|
: undefined;
|
|
34
34
|
const selected: Array<{ message: Message; index: number }> = [];
|
|
35
35
|
|
|
36
|
-
for (
|
|
36
|
+
for (
|
|
37
|
+
let i = 0;
|
|
38
|
+
i < this.#messages.length && selected.length < limitedCount;
|
|
39
|
+
i++
|
|
40
|
+
) {
|
|
37
41
|
const message = this.#messages[i];
|
|
38
42
|
if (nameSet && !nameSet.has(message.name)) {
|
|
39
43
|
continue;
|
|
@@ -64,7 +68,9 @@ class InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
async completeMessage(messageId: string): Promise<void> {
|
|
67
|
-
const index = this.#messages.findIndex(
|
|
71
|
+
const index = this.#messages.findIndex(
|
|
72
|
+
(message) => message.id === messageId,
|
|
73
|
+
);
|
|
68
74
|
if (index !== -1) {
|
|
69
75
|
this.#messages.splice(index, 1);
|
|
70
76
|
}
|
|
@@ -78,7 +84,8 @@ class InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {
|
|
|
78
84
|
throw new EvictedError();
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
const nameSet =
|
|
87
|
+
const nameSet =
|
|
88
|
+
messageNames.length > 0 ? new Set(messageNames) : undefined;
|
|
82
89
|
if (
|
|
83
90
|
this.#messages.some((message) =>
|
|
84
91
|
nameSet ? nameSet.has(message.name) : true,
|
|
@@ -103,7 +110,9 @@ class InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {
|
|
|
103
110
|
waiter.reject(new EvictedError());
|
|
104
111
|
},
|
|
105
112
|
};
|
|
106
|
-
abortSignal.addEventListener("abort", waiter.onAbort, {
|
|
113
|
+
abortSignal.addEventListener("abort", waiter.onAbort, {
|
|
114
|
+
once: true,
|
|
115
|
+
});
|
|
107
116
|
this.#waiters.add(waiter);
|
|
108
117
|
});
|
|
109
118
|
}
|
|
@@ -173,6 +182,18 @@ export class InMemoryDriver implements EngineDriver {
|
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
|
|
185
|
+
async deleteRange(start: Uint8Array, end: Uint8Array): Promise<void> {
|
|
186
|
+
await sleep(this.latency);
|
|
187
|
+
for (const [hexKey, entry] of this.kv) {
|
|
188
|
+
if (
|
|
189
|
+
compareKeys(entry.key, start) >= 0 &&
|
|
190
|
+
compareKeys(entry.key, end) < 0
|
|
191
|
+
) {
|
|
192
|
+
this.kv.delete(hexKey);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
176
197
|
async list(prefix: Uint8Array): Promise<KVEntry[]> {
|
|
177
198
|
await sleep(this.latency);
|
|
178
199
|
const results: KVEntry[] = [];
|