@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
package/src/storage.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type { EngineDriver, KVWrite } from "./driver.js";
|
|
16
16
|
import {
|
|
17
17
|
buildEntryMetadataKey,
|
|
18
|
+
buildEntryMetadataPrefix,
|
|
18
19
|
buildHistoryKey,
|
|
19
20
|
buildHistoryPrefix,
|
|
20
21
|
buildHistoryPrefixAll,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
buildWorkflowOutputKey,
|
|
25
26
|
buildWorkflowStateKey,
|
|
26
27
|
compareKeys,
|
|
28
|
+
parseEntryMetadataKey,
|
|
27
29
|
parseNameKey,
|
|
28
30
|
} from "./keys.js";
|
|
29
31
|
import { isLocationPrefix, locationToKey } from "./location.js";
|
|
@@ -129,9 +131,7 @@ export function getOrCreateMetadata(
|
|
|
129
131
|
/**
|
|
130
132
|
* Load storage from the driver.
|
|
131
133
|
*/
|
|
132
|
-
export async function loadStorage(
|
|
133
|
-
driver: EngineDriver,
|
|
134
|
-
): Promise<Storage> {
|
|
134
|
+
export async function loadStorage(driver: EngineDriver): Promise<Storage> {
|
|
135
135
|
const storage = createStorage();
|
|
136
136
|
|
|
137
137
|
// Load name registry
|
|
@@ -155,6 +155,16 @@ export async function loadStorage(
|
|
|
155
155
|
storage.history.entries.set(key, parsed);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// Load entry metadata so observers can reconstruct workflow state after
|
|
159
|
+
// the actor wakes and rebuilds storage from persisted history.
|
|
160
|
+
const metadataEntries = await driver.list(buildEntryMetadataPrefix());
|
|
161
|
+
for (const entry of metadataEntries) {
|
|
162
|
+
const entryId = parseEntryMetadataKey(entry.key);
|
|
163
|
+
const metadata = deserializeEntryMetadata(entry.value);
|
|
164
|
+
metadata.dirty = false;
|
|
165
|
+
storage.entryMetadata.set(entryId, metadata);
|
|
166
|
+
}
|
|
167
|
+
|
|
158
168
|
// Load workflow state
|
|
159
169
|
const stateValue = await driver.get(buildWorkflowStateKey());
|
|
160
170
|
if (stateValue) {
|
|
@@ -207,12 +217,25 @@ export async function loadMetadata(
|
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
/**
|
|
210
|
-
*
|
|
220
|
+
* Pending deletions collected by collectLoopPruning to be included
|
|
221
|
+
* in the next flush alongside the state write.
|
|
222
|
+
*/
|
|
223
|
+
export interface PendingDeletions {
|
|
224
|
+
prefixes: Uint8Array[];
|
|
225
|
+
keys: Uint8Array[];
|
|
226
|
+
ranges: { start: Uint8Array; end: Uint8Array }[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Flush all dirty data to the driver. Optionally includes pending
|
|
231
|
+
* deletions so that history pruning happens alongside the
|
|
232
|
+
* state write.
|
|
211
233
|
*/
|
|
212
234
|
export async function flush(
|
|
213
235
|
storage: Storage,
|
|
214
236
|
driver: EngineDriver,
|
|
215
237
|
onHistoryUpdated?: () => void,
|
|
238
|
+
pendingDeletions?: PendingDeletions,
|
|
216
239
|
): Promise<void> {
|
|
217
240
|
const writes: KVWrite[] = [];
|
|
218
241
|
let historyUpdated = false;
|
|
@@ -293,6 +316,25 @@ export async function flush(
|
|
|
293
316
|
await driver.batch(writes);
|
|
294
317
|
}
|
|
295
318
|
|
|
319
|
+
// Apply pending deletions after the batch write. These are collected
|
|
320
|
+
// by collectLoopPruning so pruning happens alongside the state write.
|
|
321
|
+
if (pendingDeletions) {
|
|
322
|
+
const deleteOps: Promise<void>[] = [];
|
|
323
|
+
for (const prefix of pendingDeletions.prefixes) {
|
|
324
|
+
deleteOps.push(driver.deletePrefix(prefix));
|
|
325
|
+
}
|
|
326
|
+
for (const range of pendingDeletions.ranges) {
|
|
327
|
+
deleteOps.push(driver.deleteRange(range.start, range.end));
|
|
328
|
+
}
|
|
329
|
+
for (const key of pendingDeletions.keys) {
|
|
330
|
+
deleteOps.push(driver.delete(key));
|
|
331
|
+
}
|
|
332
|
+
if (deleteOps.length > 0) {
|
|
333
|
+
await Promise.all(deleteOps);
|
|
334
|
+
historyUpdated = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
296
338
|
// Update flushed tracking after successful write
|
|
297
339
|
storage.flushedNameCount = storage.nameRegistry.length;
|
|
298
340
|
storage.flushedState = storage.state;
|
|
@@ -314,30 +356,41 @@ export async function deleteEntriesWithPrefix(
|
|
|
314
356
|
prefixLocation: Location,
|
|
315
357
|
onHistoryUpdated?: () => void,
|
|
316
358
|
): Promise<void> {
|
|
317
|
-
|
|
318
|
-
|
|
359
|
+
const deletions = collectDeletionsForPrefix(storage, prefixLocation);
|
|
360
|
+
|
|
361
|
+
// Apply deletions to driver
|
|
362
|
+
await driver.deletePrefix(deletions.prefixes[0]!);
|
|
363
|
+
await Promise.all(deletions.keys.map((key) => driver.delete(key)));
|
|
364
|
+
|
|
365
|
+
if (deletions.keys.length > 0 && onHistoryUpdated) {
|
|
366
|
+
onHistoryUpdated();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Remove entries matching a location prefix from memory and collect
|
|
372
|
+
* the driver-level deletion operations. The returned PendingDeletions
|
|
373
|
+
* can be applied immediately or batched with a flush.
|
|
374
|
+
*/
|
|
375
|
+
export function collectDeletionsForPrefix(
|
|
376
|
+
storage: Storage,
|
|
377
|
+
prefixLocation: Location,
|
|
378
|
+
): PendingDeletions {
|
|
379
|
+
const pending: PendingDeletions = {
|
|
380
|
+
prefixes: [buildHistoryPrefix(prefixLocation)],
|
|
381
|
+
keys: [],
|
|
382
|
+
ranges: [],
|
|
383
|
+
};
|
|
319
384
|
|
|
320
|
-
// Collect entries to delete and their IDs
|
|
321
385
|
for (const [key, entry] of storage.history.entries) {
|
|
322
|
-
// Check if the entry's location starts with the prefix location
|
|
323
386
|
if (isLocationPrefix(prefixLocation, entry.location)) {
|
|
324
|
-
|
|
387
|
+
pending.keys.push(buildEntryMetadataKey(entry.id));
|
|
325
388
|
storage.entryMetadata.delete(entry.id);
|
|
326
389
|
storage.history.entries.delete(key);
|
|
327
390
|
}
|
|
328
391
|
}
|
|
329
392
|
|
|
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
|
-
}
|
|
393
|
+
return pending;
|
|
341
394
|
}
|
|
342
395
|
|
|
343
396
|
/**
|
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[] = [];
|
package/src/types.ts
CHANGED
|
@@ -225,8 +225,7 @@ export interface WorkflowQueueNextOptions {
|
|
|
225
225
|
* Options for receiving a batch of queue messages in workflows.
|
|
226
226
|
*/
|
|
227
227
|
export interface WorkflowQueueNextBatchOptions
|
|
228
|
-
extends WorkflowQueueNextOptions
|
|
229
|
-
{
|
|
228
|
+
extends WorkflowQueueNextOptions {
|
|
230
229
|
/** Maximum number of messages to receive. Defaults to 1. */
|
|
231
230
|
count?: number;
|
|
232
231
|
}
|
|
@@ -297,6 +296,47 @@ export interface WorkflowError {
|
|
|
297
296
|
metadata?: Record<string, unknown>;
|
|
298
297
|
}
|
|
299
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Error event emitted while a workflow is running.
|
|
301
|
+
*/
|
|
302
|
+
export interface WorkflowStepErrorEvent {
|
|
303
|
+
workflowId: string;
|
|
304
|
+
stepName: string;
|
|
305
|
+
attempt: number;
|
|
306
|
+
maxRetries: number;
|
|
307
|
+
remainingRetries: number;
|
|
308
|
+
willRetry: boolean;
|
|
309
|
+
retryDelay?: number;
|
|
310
|
+
retryAt?: number;
|
|
311
|
+
error: WorkflowError;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Error event emitted when a rollback handler fails.
|
|
316
|
+
*/
|
|
317
|
+
export interface WorkflowRollbackErrorEvent {
|
|
318
|
+
workflowId: string;
|
|
319
|
+
stepName: string;
|
|
320
|
+
error: WorkflowError;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Error event emitted for workflow-level failures outside individual steps.
|
|
325
|
+
*/
|
|
326
|
+
export interface WorkflowRunErrorEvent {
|
|
327
|
+
workflowId: string;
|
|
328
|
+
error: WorkflowError;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export type WorkflowErrorEvent =
|
|
332
|
+
| { step: WorkflowStepErrorEvent }
|
|
333
|
+
| { rollback: WorkflowRollbackErrorEvent }
|
|
334
|
+
| { workflow: WorkflowRunErrorEvent };
|
|
335
|
+
|
|
336
|
+
export type WorkflowErrorHandler = (
|
|
337
|
+
event: WorkflowErrorEvent,
|
|
338
|
+
) => void | Promise<void>;
|
|
339
|
+
|
|
300
340
|
/**
|
|
301
341
|
* Complete storage state for a workflow.
|
|
302
342
|
*/
|
|
@@ -384,15 +424,11 @@ export type LoopIterationResult<S, T> = Promise<
|
|
|
384
424
|
export interface LoopConfig<S, T> {
|
|
385
425
|
name: string;
|
|
386
426
|
state?: S;
|
|
387
|
-
run: (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
/** Trim loop history every N iterations. Defaults to commitInterval or 20. */
|
|
393
|
-
historyEvery?: number;
|
|
394
|
-
/** Retain the last N iterations of history. Defaults to commitInterval or 20. */
|
|
395
|
-
historyKeep?: number;
|
|
427
|
+
run: (ctx: WorkflowContextInterface, state: S) => LoopIterationResult<S, T>;
|
|
428
|
+
/** Prune old loop iterations every N iterations. Default: 20. */
|
|
429
|
+
historyPruneInterval?: number;
|
|
430
|
+
/** Number of past iterations to retain when pruning. Defaults to historyPruneInterval. */
|
|
431
|
+
historySize?: number;
|
|
396
432
|
}
|
|
397
433
|
|
|
398
434
|
/**
|
|
@@ -458,6 +494,7 @@ export interface RunWorkflowOptions {
|
|
|
458
494
|
mode?: WorkflowRunMode;
|
|
459
495
|
logger?: Logger;
|
|
460
496
|
onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void;
|
|
497
|
+
onError?: WorkflowErrorHandler;
|
|
461
498
|
}
|
|
462
499
|
|
|
463
500
|
export type WorkflowFunction<TInput = unknown, TOutput = unknown> = (
|