@rivetkit/workflow-engine 0.0.0-main.14140ce → 0.0.0-main.17f6330

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/index.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  } from "./context.js";
14
14
  // Driver
15
15
  export type { EngineDriver, KVEntry, KVWrite } from "./driver.js";
16
+ export { extractErrorInfo } from "./error-utils.js";
16
17
  // Errors
17
18
  export {
18
19
  CancelledError,
@@ -29,7 +30,6 @@ export {
29
30
  StepExhaustedError,
30
31
  StepFailedError,
31
32
  } from "./errors.js";
32
- export { extractErrorInfo } from "./error-utils.js";
33
33
 
34
34
  // Location utilities
35
35
  export {
@@ -82,9 +82,6 @@ export type {
82
82
  PathSegment,
83
83
  RaceEntry,
84
84
  RemovedEntry,
85
- WorkflowEntryMetadataSnapshot,
86
- WorkflowHistoryEntry,
87
- WorkflowHistorySnapshot,
88
85
  RollbackCheckpointEntry,
89
86
  RollbackContextInterface,
90
87
  RunWorkflowOptions,
@@ -102,20 +99,23 @@ export type {
102
99
  TryStepFailure,
103
100
  TryStepResult,
104
101
  WorkflowContextInterface,
102
+ WorkflowEntryMetadataSnapshot,
105
103
  WorkflowError,
106
104
  WorkflowErrorEvent,
107
105
  WorkflowErrorHandler,
108
106
  WorkflowFunction,
109
107
  WorkflowHandle,
110
- WorkflowRollbackErrorEvent,
108
+ WorkflowHistoryEntry,
109
+ WorkflowHistorySnapshot,
110
+ WorkflowMessageDriver,
111
111
  WorkflowQueue,
112
112
  WorkflowQueueMessage,
113
113
  WorkflowQueueNextBatchOptions,
114
114
  WorkflowQueueNextOptions,
115
- WorkflowMessageDriver,
116
115
  WorkflowResult,
117
- WorkflowRunMode,
116
+ WorkflowRollbackErrorEvent,
118
117
  WorkflowRunErrorEvent,
118
+ WorkflowRunMode,
119
119
  WorkflowState,
120
120
  WorkflowStepErrorEvent,
121
121
  } from "./types.js";
@@ -185,9 +185,9 @@ import type {
185
185
  RunWorkflowOptions,
186
186
  Storage,
187
187
  WorkflowErrorEvent,
188
- WorkflowHistorySnapshot,
189
188
  WorkflowFunction,
190
189
  WorkflowHandle,
190
+ WorkflowHistorySnapshot,
191
191
  WorkflowMessageDriver,
192
192
  WorkflowResult,
193
193
  WorkflowRunMode,
@@ -532,26 +532,46 @@ async function executeLiveWorkflow<TInput, TOutput>(
532
532
  const hasDeadline = result.sleepUntil !== undefined;
533
533
 
534
534
  if (hasMessages && hasDeadline) {
535
- // Wait for EITHER a message OR the deadline (for queue.next timeout)
535
+ // Wait for EITHER a message OR the deadline (for queue.next timeout).
536
+ // Scope both waiters to a per-iteration controller chained to the run
537
+ // signal so that whichever loses the race is cancelled immediately.
538
+ // Otherwise the loser (a message waiter or an armed sleep timer) stays
539
+ // registered on the long-lived run signal and leaks once per cycle.
540
+ const iterationAbort = new AbortController();
541
+ const onRunAbort = () => iterationAbort.abort();
542
+ if (abortController.signal.aborted) {
543
+ iterationAbort.abort();
544
+ } else {
545
+ abortController.signal.addEventListener("abort", onRunAbort, {
546
+ once: true,
547
+ });
548
+ }
549
+ const messagePromise = awaitWithEviction(
550
+ driver.waitForMessages(
551
+ result.waitingForMessages!,
552
+ iterationAbort.signal,
553
+ ),
554
+ iterationAbort.signal,
555
+ );
556
+ const sleepPromise = waitForSleep(
557
+ runtime,
558
+ result.sleepUntil!,
559
+ iterationAbort.signal,
560
+ );
561
+ // Swallow the loser's rejection once it is aborted below so it does
562
+ // not surface as an unhandled rejection.
563
+ messagePromise.catch(() => {});
564
+ sleepPromise.catch(() => {});
536
565
  try {
537
- const messagePromise = awaitWithEviction(
538
- driver.waitForMessages(
539
- result.waitingForMessages!,
540
- abortController.signal,
541
- ),
542
- abortController.signal,
543
- );
544
- const sleepPromise = waitForSleep(
545
- runtime,
546
- result.sleepUntil!,
547
- abortController.signal,
548
- );
549
566
  await Promise.race([messagePromise, sleepPromise]);
550
567
  } catch (error) {
551
568
  if (error instanceof EvictedError) {
552
569
  return lastResult;
553
570
  }
554
571
  throw error;
572
+ } finally {
573
+ iterationAbort.abort();
574
+ abortController.signal.removeEventListener("abort", onRunAbort);
555
575
  }
556
576
  continue;
557
577
  }
package/src/location.ts CHANGED
@@ -159,7 +159,7 @@ export function getChildEntries(
159
159
  const isChild =
160
160
  parentKey === ""
161
161
  ? true
162
- : key.startsWith(parentKey + "/") || key === parentKey;
162
+ : key.startsWith(`${parentKey}/`) || key === parentKey;
163
163
 
164
164
  if (isChild) {
165
165
  // Return the actual entry's location, not the parent location
package/src/storage.ts CHANGED
@@ -40,6 +40,9 @@ import type {
40
40
  WorkflowHistorySnapshot,
41
41
  } from "./types.js";
42
42
 
43
+ export const MAX_KV_BATCH_ENTRIES = 128;
44
+ export const MAX_KV_BATCH_PAYLOAD_BYTES = 976 * 1024;
45
+
43
46
  /**
44
47
  * Create an empty storage instance.
45
48
  */
@@ -238,6 +241,8 @@ export async function flush(
238
241
  pendingDeletions?: PendingDeletions,
239
242
  ): Promise<void> {
240
243
  const writes: KVWrite[] = [];
244
+ const dirtyEntries: Entry[] = [];
245
+ const dirtyMetadata: EntryMetadata[] = [];
241
246
  let historyUpdated = false;
242
247
 
243
248
  // Flush only new names (those added since last flush)
@@ -263,7 +268,7 @@ export async function flush(
263
268
  key: buildHistoryKey(entry.location),
264
269
  value: serializeEntry(entry),
265
270
  });
266
- entry.dirty = false;
271
+ dirtyEntries.push(entry);
267
272
  historyUpdated = true;
268
273
  }
269
274
  }
@@ -275,7 +280,7 @@ export async function flush(
275
280
  key: buildEntryMetadataKey(id),
276
281
  value: serializeEntryMetadata(metadata),
277
282
  });
278
- metadata.dirty = false;
283
+ dirtyMetadata.push(metadata);
279
284
  historyUpdated = true;
280
285
  }
281
286
  }
@@ -313,7 +318,9 @@ export async function flush(
313
318
  }
314
319
 
315
320
  if (writes.length > 0) {
316
- await driver.batch(writes);
321
+ for (const chunk of splitBatchWrites(writes)) {
322
+ await driver.batch(chunk);
323
+ }
317
324
  }
318
325
 
319
326
  // Apply pending deletions after the batch write. These are collected
@@ -336,6 +343,12 @@ export async function flush(
336
343
  }
337
344
 
338
345
  // Update flushed tracking after successful write
346
+ for (const entry of dirtyEntries) {
347
+ entry.dirty = false;
348
+ }
349
+ for (const metadata of dirtyMetadata) {
350
+ metadata.dirty = false;
351
+ }
339
352
  storage.flushedNameCount = storage.nameRegistry.length;
340
353
  storage.flushedState = storage.state;
341
354
  storage.flushedOutput = storage.output;
@@ -346,6 +359,40 @@ export async function flush(
346
359
  }
347
360
  }
348
361
 
362
+ function splitBatchWrites(writes: KVWrite[]): KVWrite[][] {
363
+ const chunks: KVWrite[][] = [];
364
+ let chunk: KVWrite[] = [];
365
+ let chunkBytes = 0;
366
+
367
+ for (const write of writes) {
368
+ const writeBytes = write.key.byteLength + write.value.byteLength;
369
+ if (writeBytes > MAX_KV_BATCH_PAYLOAD_BYTES) {
370
+ throw new Error(
371
+ `KV batch write is ${writeBytes} bytes, exceeding the ${MAX_KV_BATCH_PAYLOAD_BYTES} byte limit`,
372
+ );
373
+ }
374
+
375
+ if (
376
+ chunk.length >= MAX_KV_BATCH_ENTRIES ||
377
+ (chunk.length > 0 &&
378
+ chunkBytes + writeBytes > MAX_KV_BATCH_PAYLOAD_BYTES)
379
+ ) {
380
+ chunks.push(chunk);
381
+ chunk = [];
382
+ chunkBytes = 0;
383
+ }
384
+
385
+ chunk.push(write);
386
+ chunkBytes += writeBytes;
387
+ }
388
+
389
+ if (chunk.length > 0) {
390
+ chunks.push(chunk);
391
+ }
392
+
393
+ return chunks;
394
+ }
395
+
349
396
  /**
350
397
  * Delete entries with a given location prefix (used for loop forgetting).
351
398
  * Also cleans up associated metadata from both memory and driver.
package/src/types.ts CHANGED
@@ -399,6 +399,8 @@ export interface StepConfig<T> {
399
399
  retryBackoffMax?: number;
400
400
  /** Timeout in ms for step execution (default: 30000). Set to 0 to disable. */
401
401
  timeout?: number;
402
+ /** If true, step timeouts retry like any other error instead of failing immediately as critical. Default: false. */
403
+ retryOnTimeout?: boolean;
402
404
  }
403
405
 
404
406
  export type TryStepCatchKind =
@@ -422,11 +424,7 @@ export interface TryStepConfig<T> extends StepConfig<T> {
422
424
  catch?: readonly TryStepCatchKind[];
423
425
  }
424
426
 
425
- export type TryBlockCatchKind =
426
- | "step"
427
- | "join"
428
- | "race"
429
- | "rollback";
427
+ export type TryBlockCatchKind = "step" | "join" | "race" | "rollback";
430
428
 
431
429
  export interface TryBlockFailure {
432
430
  source: "step" | "join" | "race" | "block";
@@ -459,7 +457,7 @@ export type LoopResult<S, T> =
459
457
  * `Loop.continue(undefined)`.
460
458
  */
461
459
  export type LoopIterationResult<S, T> = Promise<
462
- LoopResult<S, T> | (S extends undefined ? void : never)
460
+ LoopResult<S, T> | (S extends undefined ? undefined : never)
463
461
  >;
464
462
 
465
463
  /**
@@ -473,6 +471,12 @@ export interface LoopConfig<S, T> {
473
471
  historyPruneInterval?: number;
474
472
  /** Number of past iterations to retain when pruning. Defaults to historyPruneInterval. */
475
473
  historySize?: number;
474
+ /** @deprecated Use historyPruneInterval. */
475
+ commitInterval?: number;
476
+ /** @deprecated Use historyPruneInterval. */
477
+ historyEvery?: number;
478
+ /** @deprecated Use historySize. */
479
+ historyKeep?: number;
476
480
  }
477
481
 
478
482
  /**