@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/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
 
@@ -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,103 @@ 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(config, metadata.attempts, error, {
621
+ willRetry: true,
622
+ retryDelay,
623
+ retryAt,
624
+ });
625
+ throw new StepFailedError(
626
+ config.name,
627
+ error,
628
+ metadata.attempts,
629
+ retryAt,
630
+ );
631
+ }
632
+
633
+ const exhaustedError = markErrorReported(
634
+ new StepExhaustedError(config.name, String(error)),
635
+ );
636
+ await this.notifyStepError(config, metadata.attempts, error, {
637
+ willRetry: false,
638
+ });
639
+ throw exhaustedError;
513
640
  }
514
641
  }
515
642
 
@@ -644,7 +771,11 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
644
771
  }
645
772
 
646
773
  const loopData = existing.kind.data;
647
- metadata = await loadMetadata(this.storage, this.driver, existing.id);
774
+ metadata = await loadMetadata(
775
+ this.storage,
776
+ this.driver,
777
+ existing.id,
778
+ );
648
779
 
649
780
  if (rollbackMode) {
650
781
  if (loopData.output !== undefined) {
@@ -692,20 +823,26 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
692
823
  metadata.dirty = true;
693
824
  }
694
825
 
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;
826
+ const historyPruneInterval =
827
+ config.historyPruneInterval ?? DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL;
828
+ const historySize = config.historySize ?? historyPruneInterval;
829
+
830
+ // Track the last iteration we pruned up to so we only delete
831
+ // newly-expired iterations instead of re-scanning from 0.
832
+ let lastPrunedUpTo = 0;
833
+
834
+ // Deferred flush promise from the previous prune cycle. Awaited at the
835
+ // start of the next iteration so the flush runs in parallel with user code.
836
+ let deferredFlush: Promise<void> | null = null;
706
837
 
707
838
  // Execute loop iterations
708
839
  while (true) {
840
+ // Await any deferred flush from the previous prune cycle
841
+ if (deferredFlush) {
842
+ await deferredFlush;
843
+ deferredFlush = null;
844
+ }
845
+
709
846
  if (rollbackMode && rollbackSingleIteration) {
710
847
  if (rollbackIterationRan) {
711
848
  return rollbackOutput as T;
@@ -752,13 +889,14 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
752
889
  metadata.dirty = true;
753
890
  }
754
891
 
755
- await this.flushStorage();
756
- await this.forgetOldIterations(
892
+ // Collect pruning deletions and flush
893
+ const deletions = this.collectLoopPruning(
757
894
  location,
758
895
  iteration + 1,
759
- historyEvery,
760
- historyKeep,
896
+ historySize,
897
+ lastPrunedUpTo,
761
898
  );
899
+ await this.flushStorageWithDeletions(deletions);
762
900
 
763
901
  if (rollbackMode && rollbackSingleIteration) {
764
902
  rollbackOutput = result.value;
@@ -775,72 +913,102 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
775
913
  }
776
914
  iteration++;
777
915
 
778
- // Periodic commit
779
- if (iteration % commitInterval === 0) {
916
+ if (!rollbackMode) {
780
917
  if (entry.kind.type === "loop") {
781
918
  entry.kind.data.state = state;
782
919
  entry.kind.data.iteration = iteration;
783
920
  }
784
921
  entry.dirty = true;
922
+ }
785
923
 
786
- await this.flushStorage();
787
- await this.forgetOldIterations(
924
+ // Periodically defer the flush so the next iteration can overlap
925
+ // with loop pruning and any pending dirty state writes.
926
+ if (iteration % historyPruneInterval === 0) {
927
+ const deletions = this.collectLoopPruning(
788
928
  location,
789
929
  iteration,
790
- historyEvery,
791
- historyKeep,
930
+ historySize,
931
+ lastPrunedUpTo,
792
932
  );
933
+ lastPrunedUpTo = Math.max(0, iteration - historySize);
934
+
935
+ // Defer the flush to run in parallel with the next iteration
936
+ deferredFlush = this.flushStorageWithDeletions(deletions);
793
937
  }
794
938
  }
795
939
  }
796
940
 
797
941
  /**
798
- * Delete old loop iteration entries to save storage space.
799
- *
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:
942
+ * Collect pending deletions for loop history pruning.
803
943
  *
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.
944
+ * Only deletes iterations in the range [fromIteration, keepFrom) where
945
+ * keepFrom = currentIteration - historySize. This avoids re-scanning
946
+ * already-deleted iterations.
810
947
  */
811
- private async forgetOldIterations(
948
+ private collectLoopPruning(
812
949
  loopLocation: Location,
813
950
  currentIteration: number,
814
- historyEvery: number,
815
- historyKeep: number,
816
- ): Promise<void> {
817
- if (historyEvery <= 0 || historyKeep <= 0) {
818
- return;
951
+ historySize: number,
952
+ fromIteration: number,
953
+ ): PendingDeletions | undefined {
954
+ if (currentIteration <= historySize) {
955
+ return undefined;
819
956
  }
820
- if (currentIteration === 0 || currentIteration % historyEvery !== 0) {
821
- return;
957
+
958
+ const keepFrom = Math.max(0, currentIteration - historySize);
959
+ if (fromIteration >= keepFrom) {
960
+ return undefined;
822
961
  }
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().
962
+
827
963
  const loopSegment = loopLocation[loopLocation.length - 1];
828
964
  if (typeof loopSegment !== "number") {
829
965
  throw new Error("Expected loop location to end with a name index");
830
966
  }
831
967
 
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
- );
968
+ const range = buildLoopIterationRange(
969
+ loopLocation,
970
+ loopSegment,
971
+ fromIteration,
972
+ keepFrom,
973
+ );
974
+ const metadataKeys: Uint8Array[] = [];
975
+
976
+ for (const [key, entry] of this.storage.history.entries) {
977
+ if (!isLocationPrefix(loopLocation, entry.location)) {
978
+ continue;
979
+ }
980
+
981
+ const iterationSegment = entry.location[loopLocation.length];
982
+ if (
983
+ !iterationSegment ||
984
+ typeof iterationSegment === "number" ||
985
+ iterationSegment.loop !== loopSegment ||
986
+ iterationSegment.iteration < fromIteration ||
987
+ iterationSegment.iteration >= keepFrom
988
+ ) {
989
+ continue;
990
+ }
991
+
992
+ metadataKeys.push(buildEntryMetadataKey(entry.id));
993
+ this.storage.entryMetadata.delete(entry.id);
994
+ this.storage.history.entries.delete(key);
843
995
  }
996
+
997
+ return {
998
+ prefixes: [],
999
+ keys: metadataKeys,
1000
+ ranges: [range],
1001
+ };
1002
+ }
1003
+
1004
+ /**
1005
+ * Flush storage with optional pending deletions so pruning
1006
+ * happens alongside the state write.
1007
+ */
1008
+ private async flushStorageWithDeletions(
1009
+ deletions?: PendingDeletions,
1010
+ ): Promise<void> {
1011
+ await flush(this.storage, this.driver, this.historyNotifier, deletions);
844
1012
  }
845
1013
 
846
1014
  // === Sleep ===
@@ -1246,7 +1414,8 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1246
1414
  );
1247
1415
  const messageKey = locationToKey(this.storage, messageLocation);
1248
1416
  this.markVisited(messageKey);
1249
- const existingMessage = this.storage.history.entries.get(messageKey);
1417
+ const existingMessage =
1418
+ this.storage.history.entries.get(messageKey);
1250
1419
  if (!existingMessage || existingMessage.kind.type !== "message") {
1251
1420
  throw new HistoryDivergedError(
1252
1421
  `Expected queue message "${name}:${i}" in history`,
@@ -1267,7 +1436,9 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1267
1436
  return results;
1268
1437
  }
1269
1438
 
1270
- private toWorkflowQueueMessage<T>(message: Message): WorkflowQueueMessage<T> {
1439
+ private toWorkflowQueueMessage<T>(
1440
+ message: Message,
1441
+ ): WorkflowQueueMessage<T> {
1271
1442
  return {
1272
1443
  id: message.id,
1273
1444
  name: message.name,
@@ -1377,15 +1548,17 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1377
1548
  if (
1378
1549
  typeof value === "object" &&
1379
1550
  value !== null &&
1380
- (value as Record<string, unknown>)[QUEUE_HISTORY_MESSAGE_MARKER] === 1
1551
+ (value as Record<string, unknown>)[QUEUE_HISTORY_MESSAGE_MARKER] ===
1552
+ 1
1381
1553
  ) {
1382
1554
  const serialized = value as Record<string, unknown>;
1383
- const id =
1384
- typeof serialized.id === "string" ? serialized.id : "";
1555
+ const id = typeof serialized.id === "string" ? serialized.id : "";
1385
1556
  const serializedName =
1386
1557
  typeof serialized.name === "string" ? serialized.name : name;
1387
1558
  const createdAt =
1388
- typeof serialized.createdAt === "number" ? serialized.createdAt : 0;
1559
+ typeof serialized.createdAt === "number"
1560
+ ? serialized.createdAt
1561
+ : 0;
1389
1562
  const completed =
1390
1563
  typeof serialized.completed === "boolean"
1391
1564
  ? serialized.completed
@@ -1734,7 +1907,10 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1734
1907
  }
1735
1908
  if (error instanceof MessageWaitError) {
1736
1909
  // Track message wait errors, prefer sleep errors with deadlines
1737
- if (!yieldError || !(yieldError instanceof SleepError)) {
1910
+ if (
1911
+ !yieldError ||
1912
+ !(yieldError instanceof SleepError)
1913
+ ) {
1738
1914
  if (!yieldError) {
1739
1915
  yieldError = error;
1740
1916
  } 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
  *