@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/src/context.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { Logger } from "pino";
2
2
  import type { EngineDriver } from "./driver.js";
3
+ import {
4
+ extractErrorInfo,
5
+ getErrorEventTag,
6
+ markErrorReported,
7
+ } from "./error-utils.js";
3
8
  import {
4
9
  CancelledError,
5
10
  CriticalError,
@@ -16,10 +21,12 @@ import {
16
21
  StepExhaustedError,
17
22
  StepFailedError,
18
23
  } from "./errors.js";
24
+ import { buildLoopIterationRange, buildEntryMetadataKey } from "./keys.js";
19
25
  import {
20
26
  appendLoopIteration,
21
27
  appendName,
22
28
  emptyLocation,
29
+ isLocationPrefix,
23
30
  locationToKey,
24
31
  registerName,
25
32
  } from "./location.js";
@@ -30,6 +37,7 @@ import {
30
37
  getOrCreateMetadata,
31
38
  loadMetadata,
32
39
  setEntry,
40
+ type PendingDeletions,
33
41
  } from "./storage.js";
34
42
  import type {
35
43
  BranchConfig,
@@ -47,6 +55,8 @@ import type {
47
55
  StepConfig,
48
56
  Storage,
49
57
  WorkflowContextInterface,
58
+ WorkflowErrorEvent,
59
+ WorkflowErrorHandler,
50
60
  WorkflowQueue,
51
61
  WorkflowQueueMessage,
52
62
  WorkflowQueueNextBatchOptions,
@@ -62,9 +72,7 @@ import { sleep } from "./utils.js";
62
72
  export const DEFAULT_MAX_RETRIES = 3;
63
73
  export const DEFAULT_RETRY_BACKOFF_BASE = 100;
64
74
  export const DEFAULT_RETRY_BACKOFF_MAX = 30000;
65
- export const DEFAULT_LOOP_COMMIT_INTERVAL = 20;
66
- export const DEFAULT_LOOP_HISTORY_EVERY = 20;
67
- export const DEFAULT_LOOP_HISTORY_KEEP = 20;
75
+ export const DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL = 20;
68
76
  export const DEFAULT_STEP_TIMEOUT = 30000; // 30 seconds
69
77
 
70
78
  const QUEUE_HISTORY_MESSAGE_MARKER = "__rivetWorkflowQueueMessage";
@@ -108,7 +116,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
108
116
  private entryInProgress = false;
109
117
  private abortController: AbortController;
110
118
  private currentLocation: Location;
111
- private visitedKeys = new Set<string>();
119
+ private visitedKeys: Set<string>;
112
120
  private mode: "forward" | "rollback";
113
121
  private rollbackActions?: RollbackAction[];
114
122
  private rollbackCheckpointSet: boolean;
@@ -116,6 +124,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
116
124
  private usedNamesInExecution = new Set<string>();
117
125
  private pendingCompletableMessageIds = new Set<string>();
118
126
  private historyNotifier?: () => void;
127
+ private onError?: WorkflowErrorHandler;
119
128
  private logger?: Logger;
120
129
 
121
130
  constructor(
@@ -129,7 +138,9 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
129
138
  rollbackActions?: RollbackAction[],
130
139
  rollbackCheckpointSet = false,
131
140
  historyNotifier?: () => void,
141
+ onError?: WorkflowErrorHandler,
132
142
  logger?: Logger,
143
+ visitedKeys?: Set<string>,
133
144
  ) {
134
145
  this.currentLocation = location;
135
146
  this.abortController = abortController ?? new AbortController();
@@ -137,7 +148,9 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
137
148
  this.rollbackActions = rollbackActions;
138
149
  this.rollbackCheckpointSet = rollbackCheckpointSet;
139
150
  this.historyNotifier = historyNotifier;
151
+ this.onError = onError;
140
152
  this.logger = logger;
153
+ this.visitedKeys = visitedKeys ?? new Set();
141
154
  }
142
155
 
143
156
  get abortSignal(): AbortSignal {
@@ -147,7 +160,8 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
147
160
  get queue(): WorkflowQueue {
148
161
  return {
149
162
  next: async (name, opts) => await this.queueNext(name, opts),
150
- nextBatch: async (name, opts) => await this.queueNextBatch(name, opts),
163
+ nextBatch: async (name, opts) =>
164
+ await this.queueNextBatch(name, opts),
151
165
  send: async (name, body) => await this.queueSend(name, body),
152
166
  };
153
167
  }
@@ -190,18 +204,65 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
190
204
  this.rollbackActions,
191
205
  this.rollbackCheckpointSet,
192
206
  this.historyNotifier,
207
+ this.onError,
193
208
  this.logger,
209
+ this.visitedKeys,
194
210
  );
195
211
  }
196
212
 
197
213
  /**
198
214
  * Log a debug message using the configured logger.
199
215
  */
200
- private log(level: "debug" | "info" | "warn" | "error", data: Record<string, unknown>): void {
216
+ private log(
217
+ level: "debug" | "info" | "warn" | "error",
218
+ data: Record<string, unknown>,
219
+ ): void {
201
220
  if (!this.logger) return;
202
221
  this.logger[level](data);
203
222
  }
204
223
 
224
+ private async notifyError(event: WorkflowErrorEvent): Promise<void> {
225
+ if (!this.onError) {
226
+ return;
227
+ }
228
+
229
+ try {
230
+ await this.onError(event);
231
+ } catch (error) {
232
+ this.log("warn", {
233
+ msg: "workflow error hook failed",
234
+ hookEventType: getErrorEventTag(event),
235
+ error: extractErrorInfo(error),
236
+ });
237
+ }
238
+ }
239
+
240
+ private async notifyStepError<T>(
241
+ config: StepConfig<T>,
242
+ attempt: number,
243
+ error: unknown,
244
+ opts: {
245
+ willRetry: boolean;
246
+ retryDelay?: number;
247
+ retryAt?: number;
248
+ },
249
+ ): Promise<void> {
250
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
251
+ await this.notifyError({
252
+ step: {
253
+ workflowId: this.workflowId,
254
+ stepName: config.name,
255
+ attempt,
256
+ maxRetries,
257
+ remainingRetries: Math.max(0, maxRetries - (attempt - 1)),
258
+ willRetry: opts.willRetry,
259
+ retryDelay: opts.retryDelay,
260
+ retryAt: opts.retryAt,
261
+ error: extractErrorInfo(error),
262
+ },
263
+ });
264
+ }
265
+
205
266
  /**
206
267
  * Mark a key as visited.
207
268
  */
@@ -216,12 +277,12 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
216
277
  private checkDuplicateName(name: string): void {
217
278
  const fullKey =
218
279
  locationToKey(this.storage, this.currentLocation) + "/" + name;
219
- if (this.usedNamesInExecution.has(fullKey)) {
220
- throw new HistoryDivergedError(
221
- `Duplicate entry name "${name}" at location "${locationToKey(this.storage, this.currentLocation)}". ` +
222
- `Each step/loop/sleep/queue.next/join/race must have a unique name within its scope.`,
223
- );
224
- }
280
+ if (this.usedNamesInExecution.has(fullKey)) {
281
+ throw new HistoryDivergedError(
282
+ `Duplicate entry name "${name}" at location "${locationToKey(this.storage, this.currentLocation)}". ` +
283
+ `Each step/loop/sleep/queue.next/join/race must have a unique name within its scope.`,
284
+ );
285
+ }
225
286
  this.usedNamesInExecution.add(fullKey);
226
287
  }
227
288
 
@@ -297,7 +358,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
297
358
  // This means workflow code may have changed
298
359
  throw new HistoryDivergedError(
299
360
  `Entry "${key}" exists in history but was not visited. ` +
300
- `Workflow code may have changed. Use ctx.removed() to handle migrations.`,
361
+ `Workflow code may have changed. Use ctx.removed() to handle migrations.`,
301
362
  );
302
363
  }
303
364
  }
@@ -391,19 +452,36 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
391
452
  );
392
453
 
393
454
  // Replay successful result (including void steps).
394
- if (metadata.status === "completed" || stepData.output !== undefined) {
455
+ if (
456
+ metadata.status === "completed" ||
457
+ stepData.output !== undefined
458
+ ) {
395
459
  return stepData.output as T;
396
460
  }
397
461
 
398
462
  // Check if we should retry
399
463
  const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
400
464
 
401
- if (metadata.attempts >= maxRetries) {
465
+ if (metadata.attempts > maxRetries) {
402
466
  // Prefer step history error, but fall back to metadata since
403
467
  // driver implementations may persist metadata without the history
404
468
  // entry error (e.g. partial writes/crashes between attempts).
405
469
  const lastError = stepData.error ?? metadata.error;
406
- throw new StepExhaustedError(config.name, lastError);
470
+ const exhaustedError = markErrorReported(
471
+ new StepExhaustedError(config.name, lastError),
472
+ );
473
+ if (metadata.status !== "exhausted") {
474
+ metadata.status = "exhausted";
475
+ metadata.dirty = true;
476
+ await this.flushStorage();
477
+ await this.notifyStepError(
478
+ config,
479
+ metadata.attempts,
480
+ exhaustedError,
481
+ { willRetry: false },
482
+ );
483
+ }
484
+ throw exhaustedError;
407
485
  }
408
486
 
409
487
  // Calculate backoff and yield to scheduler
@@ -427,7 +505,11 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
427
505
  existing ?? createEntry(location, { type: "step", data: {} });
428
506
  if (!existing) {
429
507
  // New entry - register name
430
- this.log("debug", { msg: "executing new step", step: config.name, key });
508
+ this.log("debug", {
509
+ msg: "executing new step",
510
+ step: config.name,
511
+ key,
512
+ });
431
513
  const nameIndex = registerName(this.storage, config.name);
432
514
  entry.location = [...location];
433
515
  entry.location[entry.location.length - 1] = nameIndex;
@@ -437,6 +519,11 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
437
519
  }
438
520
 
439
521
  const metadata = getOrCreateMetadata(this.storage, entry.id);
522
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
523
+ const retryBackoffBase =
524
+ config.retryBackoffBase ?? DEFAULT_RETRY_BACKOFF_BASE;
525
+ const retryBackoffMax =
526
+ config.retryBackoffMax ?? DEFAULT_RETRY_BACKOFF_MAX;
440
527
  metadata.status = "running";
441
528
  metadata.attempts++;
442
529
  metadata.lastAttemptAt = Date.now();
@@ -453,63 +540,111 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
453
540
  config.name,
454
541
  );
455
542
 
456
- if (entry.kind.type === "step") {
457
- entry.kind.data.output = output;
458
- }
459
- entry.dirty = true;
460
- metadata.status = "completed";
461
- metadata.error = undefined;
462
- metadata.completedAt = Date.now();
543
+ if (entry.kind.type === "step") {
544
+ entry.kind.data.output = output;
545
+ }
546
+ entry.dirty = true;
547
+ metadata.status = "completed";
548
+ metadata.error = undefined;
549
+ metadata.completedAt = Date.now();
463
550
 
464
- // Ephemeral steps don't trigger an immediate flush. This avoids the
551
+ // Ephemeral steps don't trigger an immediate flush. This avoids the
465
552
  // synchronous write overhead for transient operations. Note that the
466
553
  // step's entry is still marked dirty and WILL be persisted on the
467
554
  // next flush from a non-ephemeral operation. The purpose of ephemeral
468
555
  // is to batch writes, not to avoid persistence entirely.
469
556
  if (!config.ephemeral) {
470
- this.log("debug", { msg: "flushing step", step: config.name, key });
557
+ this.log("debug", {
558
+ msg: "flushing step",
559
+ step: config.name,
560
+ key,
561
+ });
471
562
  await this.flushStorage();
472
563
  }
473
564
 
474
- this.log("debug", { msg: "step completed", step: config.name, key });
565
+ this.log("debug", {
566
+ msg: "step completed",
567
+ step: config.name,
568
+ key,
569
+ });
475
570
  return output;
476
571
  } catch (error) {
477
- // Timeout errors are treated as critical (no retry)
478
- if (error instanceof StepTimeoutError) {
479
- if (entry.kind.type === "step") {
480
- entry.kind.data.error = String(error);
481
- }
482
- entry.dirty = true;
483
- metadata.status = "exhausted";
484
- metadata.error = String(error);
485
- await this.flushStorage();
486
- throw new CriticalError(error.message);
572
+ // Timeout errors are treated as critical (no retry)
573
+ if (error instanceof StepTimeoutError) {
574
+ if (entry.kind.type === "step") {
575
+ entry.kind.data.error = String(error);
487
576
  }
577
+ entry.dirty = true;
578
+ metadata.status = "exhausted";
579
+ metadata.error = String(error);
580
+ await this.flushStorage();
581
+ await this.notifyStepError(config, metadata.attempts, error, {
582
+ willRetry: false,
583
+ });
584
+ throw markErrorReported(new CriticalError(error.message));
585
+ }
488
586
 
489
587
  if (
490
588
  error instanceof CriticalError ||
491
589
  error instanceof RollbackError
492
- ) {
493
- if (entry.kind.type === "step") {
494
- entry.kind.data.error = String(error);
495
- }
496
- entry.dirty = true;
497
- metadata.status = "exhausted";
498
- metadata.error = String(error);
499
- await this.flushStorage();
500
- throw error;
501
- }
502
-
590
+ ) {
503
591
  if (entry.kind.type === "step") {
504
592
  entry.kind.data.error = String(error);
505
593
  }
506
594
  entry.dirty = true;
507
- metadata.status = "failed";
595
+ metadata.status = "exhausted";
508
596
  metadata.error = String(error);
509
-
510
597
  await this.flushStorage();
598
+ await this.notifyStepError(config, metadata.attempts, error, {
599
+ willRetry: false,
600
+ });
601
+ throw markErrorReported(error);
602
+ }
603
+
604
+ if (entry.kind.type === "step") {
605
+ entry.kind.data.error = String(error);
606
+ }
607
+ entry.dirty = true;
608
+ const willRetry = metadata.attempts <= maxRetries;
609
+ metadata.status = willRetry ? "failed" : "exhausted";
610
+ metadata.error = String(error);
511
611
 
512
- throw new StepFailedError(config.name, error, metadata.attempts);
612
+ await this.flushStorage();
613
+ if (willRetry) {
614
+ const retryDelay = calculateBackoff(
615
+ metadata.attempts,
616
+ retryBackoffBase,
617
+ retryBackoffMax,
618
+ );
619
+ const retryAt = metadata.lastAttemptAt + retryDelay;
620
+ await this.notifyStepError(
621
+ config,
622
+ metadata.attempts,
623
+ error,
624
+ {
625
+ willRetry: true,
626
+ retryDelay,
627
+ retryAt,
628
+ },
629
+ );
630
+ throw new StepFailedError(
631
+ config.name,
632
+ error,
633
+ metadata.attempts,
634
+ retryAt,
635
+ );
636
+ }
637
+
638
+ const exhaustedError = markErrorReported(
639
+ new StepExhaustedError(config.name, String(error)),
640
+ );
641
+ await this.notifyStepError(
642
+ config,
643
+ metadata.attempts,
644
+ error,
645
+ { willRetry: false },
646
+ );
647
+ throw exhaustedError;
513
648
  }
514
649
  }
515
650
 
@@ -644,7 +779,11 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
644
779
  }
645
780
 
646
781
  const loopData = existing.kind.data;
647
- metadata = await loadMetadata(this.storage, this.driver, existing.id);
782
+ metadata = await loadMetadata(
783
+ this.storage,
784
+ this.driver,
785
+ existing.id,
786
+ );
648
787
 
649
788
  if (rollbackMode) {
650
789
  if (loopData.output !== undefined) {
@@ -692,20 +831,26 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
692
831
  metadata.dirty = true;
693
832
  }
694
833
 
695
- // TODO: Add validation for commitInterval (must be > 0)
696
- const commitInterval =
697
- config.commitInterval ?? DEFAULT_LOOP_COMMIT_INTERVAL;
698
- const historyEvery =
699
- config.historyEvery ??
700
- config.commitInterval ??
701
- DEFAULT_LOOP_HISTORY_EVERY;
702
- const historyKeep =
703
- config.historyKeep ??
704
- config.commitInterval ??
705
- DEFAULT_LOOP_HISTORY_KEEP;
834
+ const historyPruneInterval =
835
+ config.historyPruneInterval ?? DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL;
836
+ const historySize = config.historySize ?? historyPruneInterval;
837
+
838
+ // Track the last iteration we pruned up to so we only delete
839
+ // newly-expired iterations instead of re-scanning from 0.
840
+ let lastPrunedUpTo = 0;
841
+
842
+ // Deferred flush promise from the previous prune cycle. Awaited at the
843
+ // start of the next iteration so the flush runs in parallel with user code.
844
+ let deferredFlush: Promise<void> | null = null;
706
845
 
707
846
  // Execute loop iterations
708
847
  while (true) {
848
+ // Await any deferred flush from the previous prune cycle
849
+ if (deferredFlush) {
850
+ await deferredFlush;
851
+ deferredFlush = null;
852
+ }
853
+
709
854
  if (rollbackMode && rollbackSingleIteration) {
710
855
  if (rollbackIterationRan) {
711
856
  return rollbackOutput as T;
@@ -752,13 +897,14 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
752
897
  metadata.dirty = true;
753
898
  }
754
899
 
755
- await this.flushStorage();
756
- await this.forgetOldIterations(
900
+ // Collect pruning deletions and flush
901
+ const deletions = this.collectLoopPruning(
757
902
  location,
758
903
  iteration + 1,
759
- historyEvery,
760
- historyKeep,
904
+ historySize,
905
+ lastPrunedUpTo,
761
906
  );
907
+ await this.flushStorageWithDeletions(deletions);
762
908
 
763
909
  if (rollbackMode && rollbackSingleIteration) {
764
910
  rollbackOutput = result.value;
@@ -775,72 +921,102 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
775
921
  }
776
922
  iteration++;
777
923
 
778
- // Periodic commit
779
- if (iteration % commitInterval === 0) {
924
+ if (!rollbackMode) {
780
925
  if (entry.kind.type === "loop") {
781
926
  entry.kind.data.state = state;
782
927
  entry.kind.data.iteration = iteration;
783
928
  }
784
929
  entry.dirty = true;
930
+ }
785
931
 
786
- await this.flushStorage();
787
- await this.forgetOldIterations(
932
+ // Periodically defer the flush so the next iteration can overlap
933
+ // with loop pruning and any pending dirty state writes.
934
+ if (iteration % historyPruneInterval === 0) {
935
+ const deletions = this.collectLoopPruning(
788
936
  location,
789
937
  iteration,
790
- historyEvery,
791
- historyKeep,
938
+ historySize,
939
+ lastPrunedUpTo,
792
940
  );
941
+ lastPrunedUpTo = Math.max(0, iteration - historySize);
942
+
943
+ // Defer the flush to run in parallel with the next iteration
944
+ deferredFlush = this.flushStorageWithDeletions(deletions);
793
945
  }
794
946
  }
795
947
  }
796
948
 
797
949
  /**
798
- * Delete old loop iteration entries to save storage space.
950
+ * Collect pending deletions for loop history pruning.
799
951
  *
800
- * Loop locations always end with a NameIndex (number) because loops are
801
- * created via appendName(). Even for nested loops, the innermost loop's
802
- * location ends with its name index:
803
- *
804
- * ctx.loop("outer") → location: [outerIndex]
805
- * iteration 0 → location: [{ loop: outerIndex, iteration: 0 }]
806
- * ctx.loop("inner") → location: [{ loop: outerIndex, iteration: 0 }, innerIndex]
807
- *
808
- * This function removes iterations older than (currentIteration - historyKeep)
809
- * every historyEvery iterations.
952
+ * Only deletes iterations in the range [fromIteration, keepFrom) where
953
+ * keepFrom = currentIteration - historySize. This avoids re-scanning
954
+ * already-deleted iterations.
810
955
  */
811
- private async forgetOldIterations(
956
+ private collectLoopPruning(
812
957
  loopLocation: Location,
813
958
  currentIteration: number,
814
- historyEvery: number,
815
- historyKeep: number,
816
- ): Promise<void> {
817
- if (historyEvery <= 0 || historyKeep <= 0) {
818
- return;
959
+ historySize: number,
960
+ fromIteration: number,
961
+ ): PendingDeletions | undefined {
962
+ if (currentIteration <= historySize) {
963
+ return undefined;
819
964
  }
820
- if (currentIteration === 0 || currentIteration % historyEvery !== 0) {
821
- return;
965
+
966
+ const keepFrom = Math.max(0, currentIteration - historySize);
967
+ if (fromIteration >= keepFrom) {
968
+ return undefined;
822
969
  }
823
- const keepFrom = Math.max(0, currentIteration - historyKeep);
824
- // Get the loop name index from the last segment of loopLocation.
825
- // This is always a NameIndex (number) because loop entries are created
826
- // via appendName(), not appendLoopIteration().
970
+
827
971
  const loopSegment = loopLocation[loopLocation.length - 1];
828
972
  if (typeof loopSegment !== "number") {
829
973
  throw new Error("Expected loop location to end with a name index");
830
974
  }
831
975
 
832
- for (let i = 0; i < keepFrom; i++) {
833
- const iterationLocation: Location = [
834
- ...loopLocation,
835
- { loop: loopSegment, iteration: i },
836
- ];
837
- await deleteEntriesWithPrefix(
838
- this.storage,
839
- this.driver,
840
- iterationLocation,
841
- this.historyNotifier,
842
- );
976
+ const range = buildLoopIterationRange(
977
+ loopLocation,
978
+ loopSegment,
979
+ fromIteration,
980
+ keepFrom,
981
+ );
982
+ const metadataKeys: Uint8Array[] = [];
983
+
984
+ for (const [key, entry] of this.storage.history.entries) {
985
+ if (!isLocationPrefix(loopLocation, entry.location)) {
986
+ continue;
987
+ }
988
+
989
+ const iterationSegment = entry.location[loopLocation.length];
990
+ if (
991
+ !iterationSegment ||
992
+ typeof iterationSegment === "number" ||
993
+ iterationSegment.loop !== loopSegment ||
994
+ iterationSegment.iteration < fromIteration ||
995
+ iterationSegment.iteration >= keepFrom
996
+ ) {
997
+ continue;
998
+ }
999
+
1000
+ metadataKeys.push(buildEntryMetadataKey(entry.id));
1001
+ this.storage.entryMetadata.delete(entry.id);
1002
+ this.storage.history.entries.delete(key);
843
1003
  }
1004
+
1005
+ return {
1006
+ prefixes: [],
1007
+ keys: metadataKeys,
1008
+ ranges: [range],
1009
+ };
1010
+ }
1011
+
1012
+ /**
1013
+ * Flush storage with optional pending deletions so pruning
1014
+ * happens alongside the state write.
1015
+ */
1016
+ private async flushStorageWithDeletions(
1017
+ deletions?: PendingDeletions,
1018
+ ): Promise<void> {
1019
+ await flush(this.storage, this.driver, this.historyNotifier, deletions);
844
1020
  }
845
1021
 
846
1022
  // === Sleep ===
@@ -1246,7 +1422,8 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1246
1422
  );
1247
1423
  const messageKey = locationToKey(this.storage, messageLocation);
1248
1424
  this.markVisited(messageKey);
1249
- const existingMessage = this.storage.history.entries.get(messageKey);
1425
+ const existingMessage =
1426
+ this.storage.history.entries.get(messageKey);
1250
1427
  if (!existingMessage || existingMessage.kind.type !== "message") {
1251
1428
  throw new HistoryDivergedError(
1252
1429
  `Expected queue message "${name}:${i}" in history`,
@@ -1267,7 +1444,9 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1267
1444
  return results;
1268
1445
  }
1269
1446
 
1270
- private toWorkflowQueueMessage<T>(message: Message): WorkflowQueueMessage<T> {
1447
+ private toWorkflowQueueMessage<T>(
1448
+ message: Message,
1449
+ ): WorkflowQueueMessage<T> {
1271
1450
  return {
1272
1451
  id: message.id,
1273
1452
  name: message.name,
@@ -1377,15 +1556,17 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1377
1556
  if (
1378
1557
  typeof value === "object" &&
1379
1558
  value !== null &&
1380
- (value as Record<string, unknown>)[QUEUE_HISTORY_MESSAGE_MARKER] === 1
1559
+ (value as Record<string, unknown>)[QUEUE_HISTORY_MESSAGE_MARKER] ===
1560
+ 1
1381
1561
  ) {
1382
1562
  const serialized = value as Record<string, unknown>;
1383
- const id =
1384
- typeof serialized.id === "string" ? serialized.id : "";
1563
+ const id = typeof serialized.id === "string" ? serialized.id : "";
1385
1564
  const serializedName =
1386
1565
  typeof serialized.name === "string" ? serialized.name : name;
1387
1566
  const createdAt =
1388
- typeof serialized.createdAt === "number" ? serialized.createdAt : 0;
1567
+ typeof serialized.createdAt === "number"
1568
+ ? serialized.createdAt
1569
+ : 0;
1389
1570
  const completed =
1390
1571
  typeof serialized.completed === "boolean"
1391
1572
  ? serialized.completed
@@ -1734,7 +1915,10 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1734
1915
  }
1735
1916
  if (error instanceof MessageWaitError) {
1736
1917
  // Track message wait errors, prefer sleep errors with deadlines
1737
- if (!yieldError || !(yieldError instanceof SleepError)) {
1918
+ if (
1919
+ !yieldError ||
1920
+ !(yieldError instanceof SleepError)
1921
+ ) {
1738
1922
  if (!yieldError) {
1739
1923
  yieldError = error;
1740
1924
  } else if (yieldError instanceof MessageWaitError) {
package/src/driver.ts CHANGED
@@ -53,6 +53,11 @@ export interface EngineDriver {
53
53
  */
54
54
  deletePrefix(prefix: Uint8Array): Promise<void>;
55
55
 
56
+ /**
57
+ * Delete all keys in the half-open range [start, end).
58
+ */
59
+ deleteRange(start: Uint8Array, end: Uint8Array): Promise<void>;
60
+
56
61
  /**
57
62
  * List all key-value pairs with a given prefix.
58
63
  *