@voyantjs/workflows 0.6.7 → 0.6.9

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.
Files changed (61) hide show
  1. package/dist/auth/index.d.ts +26 -0
  2. package/dist/auth/index.d.ts.map +1 -0
  3. package/dist/auth/index.js +137 -0
  4. package/dist/conditions.d.ts +29 -0
  5. package/dist/conditions.d.ts.map +1 -0
  6. package/dist/conditions.js +5 -0
  7. package/dist/handler/index.d.ts +104 -0
  8. package/dist/handler/index.d.ts.map +1 -0
  9. package/dist/handler/index.js +238 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +10 -0
  13. package/dist/protocol/index.d.ts +187 -0
  14. package/dist/protocol/index.d.ts.map +1 -0
  15. package/dist/protocol/index.js +7 -0
  16. package/dist/rate-limit/index.d.ts +40 -0
  17. package/dist/rate-limit/index.d.ts.map +1 -0
  18. package/dist/rate-limit/index.js +139 -0
  19. package/dist/runtime/ctx.d.ts +102 -0
  20. package/dist/runtime/ctx.d.ts.map +1 -0
  21. package/dist/runtime/ctx.js +607 -0
  22. package/dist/runtime/determinism.d.ts +19 -0
  23. package/dist/runtime/determinism.d.ts.map +1 -0
  24. package/dist/runtime/determinism.js +61 -0
  25. package/dist/runtime/errors.d.ts +21 -0
  26. package/dist/runtime/errors.d.ts.map +1 -0
  27. package/dist/runtime/errors.js +45 -0
  28. package/dist/runtime/executor.d.ts +159 -0
  29. package/dist/runtime/executor.d.ts.map +1 -0
  30. package/dist/runtime/executor.js +225 -0
  31. package/dist/runtime/journal.d.ts +55 -0
  32. package/dist/runtime/journal.d.ts.map +1 -0
  33. package/dist/runtime/journal.js +28 -0
  34. package/dist/testing/index.d.ts +117 -0
  35. package/dist/testing/index.d.ts.map +1 -0
  36. package/dist/testing/index.js +595 -0
  37. package/dist/trigger.d.ts +122 -0
  38. package/dist/trigger.d.ts.map +1 -0
  39. package/dist/trigger.js +23 -0
  40. package/dist/types.d.ts +63 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +3 -0
  43. package/dist/workflow.d.ts +212 -0
  44. package/dist/workflow.d.ts.map +1 -0
  45. package/dist/workflow.js +46 -0
  46. package/package.json +30 -30
  47. package/src/auth/index.ts +46 -52
  48. package/src/conditions.ts +13 -13
  49. package/src/handler/index.ts +110 -106
  50. package/src/index.ts +7 -7
  51. package/src/protocol/index.ts +137 -71
  52. package/src/rate-limit/index.ts +77 -78
  53. package/src/runtime/ctx.ts +354 -342
  54. package/src/runtime/determinism.ts +27 -27
  55. package/src/runtime/errors.ts +17 -17
  56. package/src/runtime/executor.ts +179 -172
  57. package/src/runtime/journal.ts +25 -25
  58. package/src/testing/index.ts +268 -202
  59. package/src/trigger.ts +64 -71
  60. package/src/types.ts +16 -18
  61. package/src/workflow.ts +154 -152
@@ -3,15 +3,14 @@
3
3
  // The executor owns the waitpoint-pending queue and the callbacks
4
4
  // into the orchestrator; ctx is a thin shell that delegates.
5
5
 
6
- import type {
7
- Duration,
8
- EnvironmentName,
9
- RetryPolicy,
10
- RunTrigger,
11
- WaitpointKind,
12
- } from "../types.js";
6
+ import type { SerializedError } from "../protocol/index.js"
7
+ import type { Duration, RetryPolicy, WaitpointKind } from "../types.js"
13
8
  import type {
14
9
  EnvironmentContext,
10
+ GroupApi,
11
+ GroupScope,
12
+ InvokeApi,
13
+ InvokeOptions,
15
14
  MetadataApi,
16
15
  MetadataValue,
17
16
  ParallelApi,
@@ -21,34 +20,24 @@ import type {
21
20
  StepFn,
22
21
  StepOptions,
23
22
  StreamApi,
23
+ TokenWait,
24
+ Waitable,
24
25
  WaitForEventApi,
25
26
  WaitForSignalApi,
26
27
  WaitForTokenApi,
27
- Waitable,
28
28
  WorkflowContext,
29
29
  WorkflowHandle,
30
- InvokeApi,
31
- InvokeOptions,
32
- TokenWait,
33
- GroupApi,
34
- GroupScope,
35
- } from "../workflow.js";
36
- import type { JournalSlice, StepJournalEntry, WaitpointResolutionEntry } from "./journal.js";
37
- import type { SerializedError } from "../protocol/index.js";
30
+ } from "../workflow.js"
31
+ import { advanceClockTo, type ClockState, createRandomUUID, now } from "./determinism.js"
38
32
  import {
39
- WaitpointPendingSignal,
40
- RunCancelledSignal,
41
33
  CompensateRequestedSignal,
42
- isWaitpointPending,
43
- isRunCancelled,
44
34
  isCompensateRequested,
45
- } from "./errors.js";
46
- import {
47
- type ClockState,
48
- advanceClockTo,
49
- createRandomUUID,
50
- now,
51
- } from "./determinism.js";
35
+ isRunCancelled,
36
+ isWaitpointPending,
37
+ RunCancelledSignal,
38
+ WaitpointPendingSignal,
39
+ } from "./errors.js"
40
+ import type { JournalSlice, StepJournalEntry, WaitpointResolutionEntry } from "./journal.js"
52
41
 
53
42
  /**
54
43
  * Callbacks the executor provides for operations that must reach the
@@ -57,13 +46,13 @@ import {
57
46
  export interface RuntimeCallbacks {
58
47
  /** Run a new step and journal the result. Called only for steps not already in the journal. */
59
48
  runStep(args: {
60
- stepId: string;
61
- attempt: number;
62
- input: unknown;
63
- options: StepOptions<unknown>;
64
- fn: StepFn<unknown>;
65
- stepCtx: StepContext;
66
- }): Promise<StepJournalEntry>;
49
+ stepId: string
50
+ attempt: number
51
+ input: unknown
52
+ options: StepOptions<unknown>
53
+ fn: StepFn<unknown>
54
+ stepCtx: StepContext
55
+ }): Promise<StepJournalEntry>
67
56
 
68
57
  /**
69
58
  * Called when a step completes successfully and had a `compensate`
@@ -72,13 +61,13 @@ export interface RuntimeCallbacks {
72
61
  * is invoked.
73
62
  */
74
63
  recordCompensable(args: {
75
- stepId: string;
76
- output: unknown;
77
- compensate: (output: unknown) => Promise<void>;
78
- }): void;
64
+ stepId: string
65
+ output: unknown
66
+ compensate: (output: unknown) => Promise<void>
67
+ }): void
79
68
 
80
69
  /** Current length of the compensable list. Used by `ctx.group` checkpoints. */
81
- compensableLength(): number;
70
+ compensableLength(): number
82
71
 
83
72
  /**
84
73
  * Remove and return compensables added since `fromIndex`. Used by
@@ -86,10 +75,10 @@ export interface RuntimeCallbacks {
86
75
  * compensables.
87
76
  */
88
77
  spliceCompensable(fromIndex: number): Array<{
89
- stepId: string;
90
- output: unknown;
91
- compensate: (output: unknown) => Promise<void>;
92
- }>;
78
+ stepId: string
79
+ output: unknown
80
+ compensate: (output: unknown) => Promise<void>
81
+ }>
93
82
 
94
83
  /**
95
84
  * Called by `ctx.stream()` for each chunk produced by the source.
@@ -97,65 +86,65 @@ export interface RuntimeCallbacks {
97
86
  * journals the chunk; in tests the harness collects chunks.
98
87
  */
99
88
  pushStreamChunk(args: {
100
- streamId: string;
101
- seq: number;
102
- encoding: "text" | "json" | "base64";
103
- chunk: unknown;
104
- final: boolean;
105
- }): void;
89
+ streamId: string
90
+ seq: number
91
+ encoding: "text" | "json" | "base64"
92
+ chunk: unknown
93
+ final: boolean
94
+ }): void
106
95
 
107
96
  /** Register a new waitpoint; execution will yield after this returns. */
108
97
  registerWaitpoint(args: {
109
- clientWaitpointId: string;
110
- kind: WaitpointKind;
111
- meta: Record<string, unknown>;
112
- timeoutMs?: number;
113
- }): void;
98
+ clientWaitpointId: string
99
+ kind: WaitpointKind
100
+ meta: Record<string, unknown>
101
+ timeoutMs?: number
102
+ }): void
114
103
 
115
104
  /** Push a metadata mutation; flushed on waitpoint yield and run completion. */
116
105
  pushMetadata(op: {
117
- op: "set" | "increment" | "append" | "remove";
118
- key: string;
119
- value?: unknown;
120
- target?: "self" | "parent" | "root";
121
- }): void;
106
+ op: "set" | "increment" | "append" | "remove"
107
+ key: string
108
+ value?: unknown
109
+ target?: "self" | "parent" | "root"
110
+ }): void
122
111
 
123
112
  /** Increment invocation counter when the body resumes after eviction. */
124
- readonly invocationCount: number;
113
+ readonly invocationCount: number
125
114
 
126
115
  /** Cancellation signal exposed as `ctx.signal`. */
127
- readonly abortSignal: AbortSignal;
116
+ readonly abortSignal: AbortSignal
128
117
  }
129
118
 
130
119
  export interface RuntimeEnvironment {
131
- readonly run: RunContext;
132
- readonly workflow: { id: string; version: string };
133
- readonly environment: EnvironmentContext;
134
- readonly project: { id: string; slug: string };
135
- readonly organization: { id: string; slug: string };
120
+ readonly run: RunContext
121
+ readonly workflow: { id: string; version: string }
122
+ readonly environment: EnvironmentContext
123
+ readonly project: { id: string; slug: string }
124
+ readonly organization: { id: string; slug: string }
136
125
  }
137
126
 
138
127
  export interface CtxBuildArgs {
139
- env: RuntimeEnvironment;
140
- journal: JournalSlice;
141
- callbacks: RuntimeCallbacks;
142
- clock: ClockState;
143
- random: () => number;
128
+ env: RuntimeEnvironment
129
+ journal: JournalSlice
130
+ callbacks: RuntimeCallbacks
131
+ clock: ClockState
132
+ random: () => number
144
133
  /** Mutated as ctx.setRetry is called; each step option inherits. */
145
- retryOverride: { current: RetryPolicy | undefined };
134
+ retryOverride: { current: RetryPolicy | undefined }
146
135
  }
147
136
 
148
137
  export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
149
- const { env, journal, callbacks, clock, random, retryOverride } = args;
138
+ const { env, journal, callbacks, clock, random, retryOverride } = args
150
139
 
151
140
  // Per-ctx client-id counter. Reset on each ctx (= each invocation),
152
141
  // which means ids are stable relative to body execution order.
153
- let clientIdSeq = 0;
154
- const nextClientId = (): number => ++clientIdSeq;
142
+ let clientIdSeq = 0
143
+ const nextClientId = (): number => ++clientIdSeq
155
144
 
156
145
  function checkCancel(): void {
157
146
  if (callbacks.abortSignal.aborted) {
158
- throw new RunCancelledSignal();
147
+ throw new RunCancelledSignal()
159
148
  }
160
149
  }
161
150
 
@@ -166,14 +155,15 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
166
155
  optsOrFn: StepOptions<unknown> | StepFn<unknown>,
167
156
  maybeFn?: StepFn<unknown>,
168
157
  ) => {
169
- checkCancel();
170
- const opts: StepOptions<unknown> = typeof optsOrFn === "function" ? {} : optsOrFn;
171
- const fn: StepFn<unknown> = typeof optsOrFn === "function" ? optsOrFn : (maybeFn as StepFn<unknown>);
158
+ checkCancel()
159
+ const opts: StepOptions<unknown> = typeof optsOrFn === "function" ? {} : optsOrFn
160
+ const fn: StepFn<unknown> =
161
+ typeof optsOrFn === "function" ? optsOrFn : (maybeFn as StepFn<unknown>)
172
162
 
173
163
  // Journal hit? Return cached.
174
- const cached = journal.stepResults[id];
164
+ const cached = journal.stepResults[id]
175
165
  if (cached) {
176
- advanceClockTo(clock, cached.finishedAt);
166
+ advanceClockTo(clock, cached.finishedAt)
177
167
  if (cached.status === "ok") {
178
168
  // Re-register compensable on replay so compensations are available
179
169
  // if this invocation ends up rolling back.
@@ -182,54 +172,54 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
182
172
  stepId: id,
183
173
  output: cached.output,
184
174
  compensate: opts.compensate as (output: unknown) => Promise<void>,
185
- });
175
+ })
186
176
  }
187
- return cached.output;
177
+ return cached.output
188
178
  }
189
179
  // Journaled error rethrows on replay so catch blocks behave consistently.
190
- const e = new Error(cached.error?.message ?? "step failed");
191
- (e as { code?: string }).code = cached.error?.code;
192
- throw e;
180
+ const e = new Error(cached.error?.message ?? "step failed")
181
+ ;(e as { code?: string }).code = cached.error?.code
182
+ throw e
193
183
  }
194
184
 
195
185
  // Execute a new step via the callback, with the retry loop.
196
186
  const mergedOpts: StepOptions<unknown> = {
197
187
  ...opts,
198
188
  retry: opts.retry ?? retryOverride.current,
199
- };
200
- const policy = normalizeRetry(mergedOpts.retry);
201
- let attempt = 0;
202
- let lastEntry: StepJournalEntry | undefined;
189
+ }
190
+ const policy = normalizeRetry(mergedOpts.retry)
191
+ let attempt = 0
192
+ let lastEntry: StepJournalEntry | undefined
203
193
 
204
194
  // Per-step timeout: compose the run-level abort signal with a
205
195
  // per-call AbortSignal.timeout so cooperative step bodies (fetch,
206
196
  // setTimeout wrappers, custom AbortSignal observers) stop early
207
197
  // on timeout. Hard enforcement for uncooperative bodies is done
208
198
  // below by racing the wrapped fn against a timeout rejection.
209
- const timeoutMs = mergedOpts.timeout !== undefined ? toMs(mergedOpts.timeout) : undefined;
199
+ const timeoutMs = mergedOpts.timeout !== undefined ? toMs(mergedOpts.timeout) : undefined
210
200
  const fnWithTimeout: StepFn<unknown> =
211
201
  timeoutMs !== undefined
212
202
  ? async (stepCtx) => {
213
- let timer: ReturnType<typeof setTimeout> | undefined;
203
+ let timer: ReturnType<typeof setTimeout> | undefined
214
204
  try {
215
205
  return await Promise.race([
216
206
  fn(stepCtx),
217
207
  new Promise<never>((_, reject) => {
218
208
  timer = setTimeout(() => {
219
- const e = new Error(`step "${id}" timed out after ${timeoutMs}ms`);
220
- (e as Error & { code?: string }).code = "TIMEOUT";
221
- reject(e);
222
- }, timeoutMs);
209
+ const e = new Error(`step "${id}" timed out after ${timeoutMs}ms`)
210
+ ;(e as Error & { code?: string }).code = "TIMEOUT"
211
+ reject(e)
212
+ }, timeoutMs)
223
213
  }),
224
- ]);
214
+ ])
225
215
  } finally {
226
- if (timer !== undefined) clearTimeout(timer);
216
+ if (timer !== undefined) clearTimeout(timer)
227
217
  }
228
218
  }
229
- : fn;
219
+ : fn
230
220
 
231
221
  while (attempt < policy.max) {
232
- attempt += 1;
222
+ attempt += 1
233
223
  const stepCtx: StepContext = {
234
224
  signal:
235
225
  timeoutMs !== undefined
@@ -237,9 +227,13 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
237
227
  : callbacks.abortSignal,
238
228
  attempt,
239
229
  log: (level, msg, data) => {
240
- console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](`[${id}]`, msg, data ?? "");
230
+ console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](
231
+ `[${id}]`,
232
+ msg,
233
+ data ?? "",
234
+ )
241
235
  },
242
- };
236
+ }
243
237
  const entry = await callbacks.runStep({
244
238
  stepId: id,
245
239
  attempt,
@@ -247,42 +241,42 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
247
241
  options: mergedOpts,
248
242
  fn: fnWithTimeout,
249
243
  stepCtx,
250
- });
251
- lastEntry = entry;
244
+ })
245
+ lastEntry = entry
252
246
 
253
247
  if (entry.status === "ok") {
254
- journal.stepResults[id] = entry;
255
- advanceClockTo(clock, entry.finishedAt);
248
+ journal.stepResults[id] = entry
249
+ advanceClockTo(clock, entry.finishedAt)
256
250
  if (opts.compensate) {
257
251
  callbacks.recordCompensable({
258
252
  stepId: id,
259
253
  output: entry.output,
260
254
  compensate: opts.compensate as (output: unknown) => Promise<void>,
261
- });
255
+ })
262
256
  }
263
- return entry.output;
257
+ return entry.output
264
258
  }
265
259
 
266
260
  // Failed attempt. Check if we should stop retrying.
267
- if (entry.error?.code === "FATAL") break;
268
- if (attempt >= policy.max) break;
261
+ if (entry.error?.code === "FATAL") break
262
+ if (attempt >= policy.max) break
269
263
 
270
264
  // In production the step handler returns { retryAfter } to the DO
271
265
  // which sets an alarm; here the spike/test harness continues
272
266
  // immediately. retryAfter from RetryableError wins over the policy
273
267
  // backoff when set.
274
- const retryAfter = readRetryAfter(entry.error);
275
- await maybeDelay(retryAfter ?? backoffDelay(policy, attempt));
268
+ const retryAfter = readRetryAfter(entry.error)
269
+ await maybeDelay(retryAfter ?? backoffDelay(policy, attempt))
276
270
  }
277
271
 
278
272
  // Retries exhausted (or never retried).
279
- const finalEntry = lastEntry!;
280
- journal.stepResults[id] = finalEntry;
281
- advanceClockTo(clock, finalEntry.finishedAt);
282
- const e = new Error(finalEntry.error?.message ?? "step failed");
283
- (e as { code?: string }).code = finalEntry.error?.code;
284
- throw e;
285
- }) as StepApi;
273
+ const finalEntry = lastEntry!
274
+ journal.stepResults[id] = finalEntry
275
+ advanceClockTo(clock, finalEntry.finishedAt)
276
+ const e = new Error(finalEntry.error?.message ?? "step failed")
277
+ ;(e as { code?: string }).code = finalEntry.error?.code
278
+ throw e
279
+ }) as StepApi
286
280
 
287
281
  // ---- waits ----
288
282
 
@@ -292,25 +286,25 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
292
286
  meta: Record<string, unknown>,
293
287
  timeoutMs?: number,
294
288
  ): never {
295
- callbacks.registerWaitpoint({ clientWaitpointId, kind, meta, timeoutMs });
296
- throw new WaitpointPendingSignal(clientWaitpointId);
289
+ callbacks.registerWaitpoint({ clientWaitpointId, kind, meta, timeoutMs })
290
+ throw new WaitpointPendingSignal(clientWaitpointId)
297
291
  }
298
292
 
299
293
  function lookupWaitpoint(id: string): WaitpointResolutionEntry | undefined {
300
- return journal.waitpointsResolved[id];
294
+ return journal.waitpointsResolved[id]
301
295
  }
302
296
 
303
297
  const sleep = async (duration: Duration): Promise<void> => {
304
- checkCancel();
305
- const id = `sleep:${nextClientId()}`;
306
- const resolved = lookupWaitpoint(id);
298
+ checkCancel()
299
+ const id = `sleep:${nextClientId()}`
300
+ const resolved = lookupWaitpoint(id)
307
301
  if (resolved) {
308
- advanceClockTo(clock, resolved.resolvedAt);
309
- return;
302
+ advanceClockTo(clock, resolved.resolvedAt)
303
+ return
310
304
  }
311
- const ms = toMs(duration);
312
- yieldWaitpoint(id, "DATETIME", { durationMs: ms }, ms);
313
- };
305
+ const ms = toMs(duration)
306
+ yieldWaitpoint(id, "DATETIME", { durationMs: ms }, ms)
307
+ }
314
308
 
315
309
  function makeWaitable<T>(
316
310
  kind: WaitpointKind,
@@ -322,69 +316,70 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
322
316
  ): Waitable<T> {
323
317
  // --- thenable: single first-match-wins resolution ---
324
318
  const resolve = (): T | null => {
325
- const resolved = lookupWaitpoint(clientWaitpointId);
319
+ const resolved = lookupWaitpoint(clientWaitpointId)
326
320
  if (!resolved) {
327
- yieldWaitpoint(clientWaitpointId, kind, meta, timeoutMs);
321
+ yieldWaitpoint(clientWaitpointId, kind, meta, timeoutMs)
328
322
  }
329
- advanceClockTo(clock, resolved.resolvedAt);
323
+ advanceClockTo(clock, resolved.resolvedAt)
330
324
  if (resolved.payload === undefined && onTimeout === "throw") {
331
- throw new Error(`waitpoint ${clientWaitpointId} timed out`);
325
+ throw new Error(`waitpoint ${clientWaitpointId} timed out`)
332
326
  }
333
- return (resolved.payload ?? null) as T | null;
334
- };
327
+ return (resolved.payload ?? null) as T | null
328
+ }
335
329
 
336
330
  // --- iterable: fresh waitpoint per .next() call ---
337
331
  function makeIterator(): AsyncIterableIterator<T> {
338
- let closed = false;
332
+ let closed = false
339
333
  return {
340
334
  async next(): Promise<IteratorResult<T>> {
341
- if (closed) return { value: undefined as unknown as T, done: true };
342
- checkCancel();
343
- const iterId = `${iterIdPrefix}:iter:${nextClientId()}`;
344
- const resolvedIter = lookupWaitpoint(iterId);
335
+ if (closed) return { value: undefined as unknown as T, done: true }
336
+ checkCancel()
337
+ const iterId = `${iterIdPrefix}:iter:${nextClientId()}`
338
+ const resolvedIter = lookupWaitpoint(iterId)
345
339
  if (!resolvedIter) {
346
- yieldWaitpoint(iterId, kind, { ...meta, iter: true }, timeoutMs);
340
+ yieldWaitpoint(iterId, kind, { ...meta, iter: true }, timeoutMs)
347
341
  }
348
- advanceClockTo(clock, resolvedIter.resolvedAt);
342
+ advanceClockTo(clock, resolvedIter.resolvedAt)
349
343
  // End-of-stream marker. Harness / orchestrator writes this to
350
344
  // tell the iterator the source has no more events.
351
- const payload = resolvedIter.payload as unknown;
345
+ const payload = resolvedIter.payload as unknown
352
346
  if (isStreamEnd(payload)) {
353
- closed = true;
354
- return { value: undefined as unknown as T, done: true };
347
+ closed = true
348
+ return { value: undefined as unknown as T, done: true }
355
349
  }
356
350
  if (payload === undefined && onTimeout === "throw") {
357
- throw new Error(`waitpoint ${iterId} timed out`);
351
+ throw new Error(`waitpoint ${iterId} timed out`)
358
352
  }
359
- return { value: payload as T, done: false };
353
+ return { value: payload as T, done: false }
360
354
  },
361
355
  async return(): Promise<IteratorResult<T>> {
362
- closed = true;
363
- return { value: undefined as unknown as T, done: true };
356
+ closed = true
357
+ return { value: undefined as unknown as T, done: true }
364
358
  },
365
359
  [Symbol.asyncIterator]() {
366
- return this;
360
+ return this
367
361
  },
368
- };
362
+ }
369
363
  }
370
364
 
371
365
  const thenable: Waitable<T> = {
366
+ // biome-ignore lint/suspicious/noThenProperty: Waitable intentionally implements PromiseLike for `await`.
372
367
  then(onFulfilled, onRejected) {
373
368
  try {
374
- const r = resolve();
375
- return Promise.resolve(r).then(onFulfilled, onRejected);
369
+ const r = resolve()
370
+ return Promise.resolve(r).then(onFulfilled, onRejected)
376
371
  } catch (e) {
377
- return Promise.reject(e).then(onFulfilled, onRejected);
372
+ return Promise.reject(e).then(onFulfilled, onRejected)
378
373
  }
379
374
  },
380
375
  [Symbol.asyncIterator]() {
381
- return makeIterator();
376
+ return makeIterator()
382
377
  },
383
378
  close() {
384
379
  // no-op; `return()` on the iterator handles early break.
385
380
  },
386
- };
387
- return thenable;
381
+ }
382
+ return thenable
388
383
  }
389
384
 
390
385
  function isStreamEnd(payload: unknown): boolean {
@@ -392,13 +387,16 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
392
387
  typeof payload === "object" &&
393
388
  payload !== null &&
394
389
  (payload as { __voyantStreamEnd?: boolean }).__voyantStreamEnd === true
395
- );
390
+ )
396
391
  }
397
392
 
398
- const waitForEvent: WaitForEventApi = ((eventType: string, opts?: { timeout?: Duration; onTimeout?: "null" | "throw" }) => {
399
- checkCancel();
400
- const thenableId = `event:${eventType}:${nextClientId()}`;
401
- const iterPrefix = `event:${eventType}`;
393
+ const waitForEvent: WaitForEventApi = ((
394
+ eventType: string,
395
+ opts?: { timeout?: Duration; onTimeout?: "null" | "throw" },
396
+ ) => {
397
+ checkCancel()
398
+ const thenableId = `event:${eventType}:${nextClientId()}`
399
+ const iterPrefix = `event:${eventType}`
402
400
  return makeWaitable(
403
401
  "EVENT",
404
402
  thenableId,
@@ -406,13 +404,16 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
406
404
  { eventType },
407
405
  opts?.timeout ? toMs(opts.timeout) : undefined,
408
406
  opts?.onTimeout,
409
- );
410
- }) as WaitForEventApi;
407
+ )
408
+ }) as WaitForEventApi
411
409
 
412
- const waitForSignal: WaitForSignalApi = ((name: string, opts?: { timeout?: Duration; onTimeout?: "null" | "throw" }) => {
413
- checkCancel();
414
- const thenableId = `signal:${name}:${nextClientId()}`;
415
- const iterPrefix = `signal:${name}`;
410
+ const waitForSignal: WaitForSignalApi = ((
411
+ name: string,
412
+ opts?: { timeout?: Duration; onTimeout?: "null" | "throw" },
413
+ ) => {
414
+ checkCancel()
415
+ const thenableId = `signal:${name}:${nextClientId()}`
416
+ const iterPrefix = `signal:${name}`
416
417
  return makeWaitable(
417
418
  "SIGNAL",
418
419
  thenableId,
@@ -420,39 +421,39 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
420
421
  { signalName: name },
421
422
  opts?.timeout ? toMs(opts.timeout) : undefined,
422
423
  opts?.onTimeout,
423
- );
424
- }) as WaitForSignalApi;
424
+ )
425
+ }) as WaitForSignalApi
425
426
 
426
427
  const waitForToken: WaitForTokenApi = (async (opts?: {
427
- tokenId?: string;
428
- timeout?: Duration;
429
- onTimeout?: "null" | "throw";
428
+ tokenId?: string
429
+ timeout?: Duration
430
+ onTimeout?: "null" | "throw"
430
431
  }) => {
431
- checkCancel();
432
+ checkCancel()
432
433
  // Allocate a stable id per call. User-supplied `tokenId` is kept
433
434
  // verbatim so external systems can reference the same value.
434
- const tokenId = opts?.tokenId ?? `tok_${nextClientId()}`;
435
- const waitpointId = `token:${tokenId}`;
436
- const timeoutMs = opts?.timeout ? toMs(opts.timeout) : undefined;
437
- const onTimeout = opts?.onTimeout ?? "null";
435
+ const tokenId = opts?.tokenId ?? `tok_${nextClientId()}`
436
+ const waitpointId = `token:${tokenId}`
437
+ const timeoutMs = opts?.timeout ? toMs(opts.timeout) : undefined
438
+ const onTimeout = opts?.onTimeout ?? "null"
438
439
 
439
440
  return {
440
441
  tokenId,
441
442
  url: `/__voyant/tokens/${tokenId}`,
442
443
  wait: async (): Promise<unknown> => {
443
- checkCancel();
444
- const resolved = lookupWaitpoint(waitpointId);
444
+ checkCancel()
445
+ const resolved = lookupWaitpoint(waitpointId)
445
446
  if (resolved) {
446
- advanceClockTo(clock, resolved.resolvedAt);
447
+ advanceClockTo(clock, resolved.resolvedAt)
447
448
  if (resolved.payload === undefined && onTimeout === "throw") {
448
- throw new Error(`token ${tokenId} timed out`);
449
+ throw new Error(`token ${tokenId} timed out`)
449
450
  }
450
- return resolved.payload ?? null;
451
+ return resolved.payload ?? null
451
452
  }
452
- yieldWaitpoint(waitpointId, "MANUAL", { tokenId }, timeoutMs);
453
+ yieldWaitpoint(waitpointId, "MANUAL", { tokenId }, timeoutMs)
453
454
  },
454
- } as TokenWait<unknown>;
455
- }) as WaitForTokenApi;
455
+ } as TokenWait<unknown>
456
+ }) as WaitForTokenApi
456
457
 
457
458
  // ---- invoke / parallel ----
458
459
 
@@ -461,17 +462,17 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
461
462
  input: TIn,
462
463
  opts?: InvokeOptions,
463
464
  ): Promise<TOut> => {
464
- checkCancel();
465
- const id = `invoke:${wf.id}:${nextClientId()}`;
466
- const resolved = journal.waitpointsResolved[id];
465
+ checkCancel()
466
+ const id = `invoke:${wf.id}:${nextClientId()}`
467
+ const resolved = journal.waitpointsResolved[id]
467
468
  if (resolved) {
468
- advanceClockTo(clock, resolved.resolvedAt);
469
+ advanceClockTo(clock, resolved.resolvedAt)
469
470
  if (resolved.error) {
470
- const e = new Error(resolved.error.message);
471
- (e as { code?: string }).code = resolved.error.code;
472
- throw e;
471
+ const e = new Error(resolved.error.message)
472
+ ;(e as { code?: string }).code = resolved.error.code
473
+ throw e
473
474
  }
474
- return resolved.payload as TOut;
475
+ return resolved.payload as TOut
475
476
  }
476
477
  yieldWaitpoint(id, "RUN", {
477
478
  childWorkflowId: wf.id,
@@ -480,133 +481,131 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
480
481
  tags: opts?.tags ?? [],
481
482
  lockToVersion: opts?.lockToVersion,
482
483
  idempotencyKey: opts?.idempotencyKey,
483
- });
484
- }) as InvokeApi;
484
+ })
485
+ }) as InvokeApi
485
486
 
486
487
  const parallel: ParallelApi = async <T, R>(
487
488
  items: readonly T[],
488
489
  fn: (item: T, index: number) => Promise<R>,
489
490
  opts?: { concurrency?: number; settle?: boolean },
490
491
  ): Promise<R[]> => {
491
- checkCancel();
492
- const total = items.length;
493
- if (total === 0) return [];
494
- const concurrency = Math.max(1, opts?.concurrency ?? total);
495
- const settle = opts?.settle ?? false;
492
+ checkCancel()
493
+ const total = items.length
494
+ if (total === 0) return []
495
+ const concurrency = Math.max(1, opts?.concurrency ?? total)
496
+ const settle = opts?.settle ?? false
496
497
 
497
- const results: R[] = new Array(total);
498
- const errors: { index: number; error: unknown }[] = [];
499
- let cursor = 0;
500
- let aborted = false;
498
+ const results: R[] = new Array(total)
499
+ const errors: { index: number; error: unknown }[] = []
500
+ let cursor = 0
501
+ let aborted = false
501
502
 
502
503
  async function worker(): Promise<void> {
503
504
  while (!aborted) {
504
- const i = cursor++;
505
- if (i >= total) return;
505
+ const i = cursor++
506
+ if (i >= total) return
506
507
  try {
507
- results[i] = await fn(items[i]!, i);
508
+ results[i] = await fn(items[i]!, i)
508
509
  } catch (err) {
509
510
  if (settle) {
510
- errors.push({ index: i, error: err });
511
+ errors.push({ index: i, error: err })
511
512
  } else {
512
- aborted = true;
513
- throw err;
513
+ aborted = true
514
+ throw err
514
515
  }
515
516
  }
516
517
  }
517
518
  }
518
519
 
519
- const workerCount = Math.min(concurrency, total);
520
- const workers = Array.from({ length: workerCount }, () => worker());
520
+ const workerCount = Math.min(concurrency, total)
521
+ const workers = Array.from({ length: workerCount }, () => worker())
521
522
 
522
523
  if (settle) {
523
- await Promise.all(workers);
524
+ await Promise.all(workers)
524
525
  if (errors.length > 0) {
525
526
  // Attach details so callers can inspect which items failed.
526
527
  const agg = new AggregateError(
527
528
  errors.map((e) => (e.error instanceof Error ? e.error : new Error(String(e.error)))),
528
529
  `ctx.parallel: ${errors.length}/${total} iteration${errors.length === 1 ? "" : "s"} failed`,
529
- );
530
- (agg as { failedIndices?: number[] }).failedIndices = errors.map((e) => e.index);
531
- throw agg;
530
+ )
531
+ ;(agg as { failedIndices?: number[] }).failedIndices = errors.map((e) => e.index)
532
+ throw agg
532
533
  }
533
- return results;
534
+ return results
534
535
  }
535
536
 
536
- await Promise.all(workers);
537
- return results;
538
- };
537
+ await Promise.all(workers)
538
+ return results
539
+ }
539
540
 
540
541
  // ---- streams ----
541
542
 
542
- const activeStreamIds = new Set<string>();
543
+ const activeStreamIds = new Set<string>()
543
544
 
544
545
  async function consumeStream(
545
546
  streamId: string,
546
547
  source: AsyncIterable<unknown>,
547
548
  encoding: "text" | "json" | "base64",
548
549
  ): Promise<void> {
549
- checkCancel();
550
+ checkCancel()
550
551
  if (activeStreamIds.has(streamId)) {
551
- throw new Error(`ctx.stream: duplicate streamId "${streamId}" within the same run`);
552
+ throw new Error(`ctx.stream: duplicate streamId "${streamId}" within the same run`)
552
553
  }
553
- activeStreamIds.add(streamId);
554
+ activeStreamIds.add(streamId)
554
555
  // Replay skip: the prior invocation already drained this source
555
556
  // and the orchestrator has the chunks. Re-iterating would double
556
557
  // any side effects (LLM calls, billable APIs, file reads).
557
558
  if (journal.streamsCompleted[streamId]) {
558
- return;
559
+ return
559
560
  }
560
- let seq = 0;
561
- const iter = source[Symbol.asyncIterator]();
561
+ let seq = 0
562
+ const iter = source[Symbol.asyncIterator]()
562
563
  try {
563
564
  while (true) {
564
- checkCancel();
565
- const { value, done } = await iter.next();
565
+ checkCancel()
566
+ const { value, done } = await iter.next()
566
567
  if (done) {
567
- callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true });
568
- journal.streamsCompleted[streamId] = { chunkCount: seq + 1 };
569
- return;
568
+ callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true })
569
+ journal.streamsCompleted[streamId] = { chunkCount: seq + 1 }
570
+ return
570
571
  }
571
- seq += 1;
572
- const chunk = normalizeChunk(value, encoding);
573
- callbacks.pushStreamChunk({ streamId, seq, encoding, chunk, final: false });
572
+ seq += 1
573
+ const chunk = normalizeChunk(value, encoding)
574
+ callbacks.pushStreamChunk({ streamId, seq, encoding, chunk, final: false })
574
575
  }
575
576
  } catch (err) {
576
577
  // Emit a final frame so consumers know the stream closed, then
577
578
  // propagate so the workflow body's error handling kicks in. No
578
579
  // journal entry — a failed stream should re-iterate on replay
579
580
  // (so the error surfaces deterministically).
580
- callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true });
581
- throw err;
581
+ callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true })
582
+ throw err
582
583
  }
583
584
  }
584
585
 
585
586
  const streamImpl = async (
586
587
  streamId: string,
587
- sourceOrFn:
588
- | AsyncIterable<unknown>
589
- | (() => AsyncGenerator<unknown>),
588
+ sourceOrFn: AsyncIterable<unknown> | (() => AsyncGenerator<unknown>),
590
589
  ): Promise<void> => {
591
590
  const source =
592
591
  typeof sourceOrFn === "function"
593
592
  ? (sourceOrFn as () => AsyncGenerator<unknown>)()
594
- : sourceOrFn;
595
- await consumeStream(streamId, source, inferEncoding(source));
596
- };
593
+ : sourceOrFn
594
+ await consumeStream(streamId, source, inferEncoding(source))
595
+ }
597
596
 
598
597
  // Typed shape variants. Each forwards to consumeStream with a fixed encoding.
599
- (streamImpl as unknown as { text: StreamApi["text"] }).text = async (id, source) => {
600
- await consumeStream(id, source, "text");
601
- };
602
- (streamImpl as unknown as { json: StreamApi["json"] }).json = async (id, source) => {
603
- await consumeStream(id, source, "json");
604
- };
605
- (streamImpl as unknown as { bytes: StreamApi["bytes"] }).bytes = async (id, source) => {
606
- await consumeStream(id, source, "base64");
607
- };
608
-
609
- const stream = streamImpl as unknown as StreamApi;
598
+ ;(streamImpl as unknown as { text: StreamApi["text"] }).text = async (id, source) => {
599
+ await consumeStream(id, source, "text")
600
+ }
601
+ ;(streamImpl as unknown as { json: StreamApi["json"] }).json = async (id, source) => {
602
+ await consumeStream(id, source, "json")
603
+ }
604
+ ;(streamImpl as unknown as { bytes: StreamApi["bytes"] }).bytes = async (id, source) => {
605
+ await consumeStream(id, source, "base64")
606
+ }
607
+
608
+ const stream = streamImpl as unknown as StreamApi
610
609
 
611
610
  // ---- groups ----
612
611
 
@@ -621,11 +620,11 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
621
620
  // outer list — they'll still be rolled back if the enclosing workflow
622
621
  // later throws.
623
622
  const runScopedCompensations = async (fromIndex: number): Promise<void> => {
624
- const scopeEntries = callbacks.spliceCompensable(fromIndex);
623
+ const scopeEntries = callbacks.spliceCompensable(fromIndex)
625
624
  for (let i = scopeEntries.length - 1; i >= 0; i--) {
626
- const c = scopeEntries[i]!;
625
+ const c = scopeEntries[i]!
627
626
  try {
628
- await c.compensate(c.output);
627
+ await c.compensate(c.output)
629
628
  } catch {
630
629
  // One bad compensation in a scope does not abort the others.
631
630
  // Errors here don't surface to the executor — the outer rollback
@@ -633,54 +632,58 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
633
632
  // unwind.
634
633
  }
635
634
  }
636
- };
635
+ }
637
636
 
638
637
  const group: GroupApi = async <T>(
639
638
  _name: string,
640
639
  fn: (scope: GroupScope) => Promise<T>,
641
640
  ): Promise<T> => {
642
- checkCancel();
643
- const checkpointStart = callbacks.compensableLength();
641
+ checkCancel()
642
+ const checkpointStart = callbacks.compensableLength()
644
643
  try {
645
644
  return await fn({
646
645
  step,
647
646
  compensate: async (): Promise<never> => {
648
- await runScopedCompensations(checkpointStart);
649
- throw new CompensateRequestedSignal();
647
+ await runScopedCompensations(checkpointStart)
648
+ throw new CompensateRequestedSignal()
650
649
  },
651
- });
650
+ })
652
651
  } catch (err) {
653
652
  // Only run scoped compensations for real user errors — internal
654
653
  // signals (waitpoint yield, cancellation, compensate-requested)
655
654
  // are re-thrown unchanged so the executor can route them.
656
- if (
657
- !isWaitpointPending(err) &&
658
- !isRunCancelled(err) &&
659
- !isCompensateRequested(err)
660
- ) {
661
- await runScopedCompensations(checkpointStart);
655
+ if (!isWaitpointPending(err) && !isRunCancelled(err) && !isCompensateRequested(err)) {
656
+ await runScopedCompensations(checkpointStart)
662
657
  }
663
- throw err;
658
+ throw err
664
659
  }
665
- };
660
+ }
666
661
 
667
662
  // ---- metadata ----
668
663
 
669
664
  const metadata: MetadataApi = {
670
- set(key, value) { callbacks.pushMetadata({ op: "set", key, value }); },
671
- increment(key, by = 1) { callbacks.pushMetadata({ op: "increment", key, value: by }); },
672
- append(key, value) { callbacks.pushMetadata({ op: "append", key, value }); },
673
- remove(key) { callbacks.pushMetadata({ op: "remove", key }); },
665
+ set(key, value) {
666
+ callbacks.pushMetadata({ op: "set", key, value })
667
+ },
668
+ increment(key, by = 1) {
669
+ callbacks.pushMetadata({ op: "increment", key, value: by })
670
+ },
671
+ append(key, value) {
672
+ callbacks.pushMetadata({ op: "append", key, value })
673
+ },
674
+ remove(key) {
675
+ callbacks.pushMetadata({ op: "remove", key })
676
+ },
674
677
  // Mutations are pushed immediately via `callbacks.pushMetadata`
675
678
  // and collected on the response envelope; no explicit flush is
676
679
  // needed.
677
680
  flush: async () => {},
678
- };
681
+ }
679
682
 
680
683
  // ---- retry override ----
681
684
 
682
685
  function setRetry(policy: RetryPolicy): void {
683
- retryOverride.current = policy;
686
+ retryOverride.current = policy
684
687
  }
685
688
 
686
689
  return {
@@ -706,10 +709,10 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
706
709
  randomUUID: createRandomUUID(random),
707
710
  setRetry,
708
711
  compensate: async (): Promise<never> => {
709
- checkCancel();
710
- throw new CompensateRequestedSignal();
712
+ checkCancel()
713
+ throw new CompensateRequestedSignal()
711
714
  },
712
- } satisfies WorkflowContext<unknown>;
715
+ } satisfies WorkflowContext<unknown>
713
716
  }
714
717
 
715
718
  // ---- helpers ----
@@ -717,92 +720,94 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
717
720
  function inferEncoding(source: unknown): "text" | "json" | "base64" {
718
721
  // Default to json for the generic ctx.stream(id, generator) call. The
719
722
  // typed variants (text/json/bytes) override this.
720
- void source;
721
- return "json";
723
+ void source
724
+ return "json"
722
725
  }
723
726
 
724
727
  function normalizeChunk(value: unknown, encoding: "text" | "json" | "base64"): unknown {
725
728
  if (encoding === "text") {
726
- return typeof value === "string" ? value : String(value);
729
+ return typeof value === "string" ? value : String(value)
727
730
  }
728
731
  if (encoding === "base64") {
729
732
  if (value instanceof Uint8Array) {
730
- return toBase64(value);
733
+ return toBase64(value)
731
734
  }
732
- throw new Error("ctx.stream.bytes: expected Uint8Array chunks");
735
+ throw new Error("ctx.stream.bytes: expected Uint8Array chunks")
733
736
  }
734
- return value; // json — pass through
737
+ return value // json — pass through
735
738
  }
736
739
 
737
740
  function toBase64(bytes: Uint8Array): string {
738
741
  // Node + modern runtimes provide Buffer or btoa. Use Buffer when
739
742
  // available for efficiency; fall back to manual encode for isolates.
740
743
  const g = globalThis as unknown as {
741
- Buffer?: { from(b: Uint8Array): { toString(enc: "base64"): string } };
742
- btoa?: (s: string) => string;
743
- };
744
- if (g.Buffer) return g.Buffer.from(bytes).toString("base64");
744
+ Buffer?: { from(b: Uint8Array): { toString(enc: "base64"): string } }
745
+ btoa?: (s: string) => string
746
+ }
747
+ if (g.Buffer) return g.Buffer.from(bytes).toString("base64")
745
748
  if (g.btoa) {
746
- let s = "";
747
- for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!);
748
- return g.btoa(s);
749
+ let s = ""
750
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!)
751
+ return g.btoa(s)
749
752
  }
750
753
  // Manual fallback (rare).
751
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
752
- let out = "";
753
- let i = 0;
754
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
755
+ let out = ""
756
+ let i = 0
754
757
  while (i < bytes.length) {
755
- const b1 = bytes[i++]!;
756
- const b2 = i < bytes.length ? bytes[i++]! : 0;
757
- const b3 = i < bytes.length ? bytes[i++]! : 0;
758
- out += chars[b1 >> 2]!;
759
- out += chars[((b1 & 3) << 4) | (b2 >> 4)]!;
760
- out += i - 1 > bytes.length ? "=" : chars[((b2 & 15) << 2) | (b3 >> 6)]!;
761
- out += i > bytes.length ? "=" : chars[b3 & 63]!;
758
+ const b1 = bytes[i++]!
759
+ const b2 = i < bytes.length ? bytes[i++]! : 0
760
+ const b3 = i < bytes.length ? bytes[i++]! : 0
761
+ out += chars[b1 >> 2]!
762
+ out += chars[((b1 & 3) << 4) | (b2 >> 4)]!
763
+ out += i - 1 > bytes.length ? "=" : chars[((b2 & 15) << 2) | (b3 >> 6)]!
764
+ out += i > bytes.length ? "=" : chars[b3 & 63]!
762
765
  }
763
- return out;
766
+ return out
764
767
  }
765
768
 
766
769
  interface ResolvedRetryPolicy {
767
- max: number;
768
- backoff: "exponential" | "linear" | "fixed";
769
- initial: number; // ms
770
- maxDelay: number; // ms
770
+ max: number
771
+ backoff: "exponential" | "linear" | "fixed"
772
+ initial: number // ms
773
+ maxDelay: number // ms
771
774
  }
772
775
 
773
- function normalizeRetry(
774
- input: RetryPolicy | { max: 0 } | undefined,
775
- ): ResolvedRetryPolicy {
776
- if (!input) return { max: 1, backoff: "exponential", initial: 1000, maxDelay: 60_000 };
777
- const max = input.max ?? 3;
778
- const policy = input as RetryPolicy;
776
+ function normalizeRetry(input: RetryPolicy | { max: 0 } | undefined): ResolvedRetryPolicy {
777
+ if (!input) return { max: 1, backoff: "exponential", initial: 1000, maxDelay: 60_000 }
778
+ const max = input.max ?? 3
779
+ const policy = input as RetryPolicy
779
780
  return {
780
781
  max: Math.max(1, max),
781
782
  backoff: policy.backoff ?? "exponential",
782
783
  initial: policy.initial !== undefined ? toMs(policy.initial) : 1000,
783
784
  maxDelay: policy.maxDelay !== undefined ? toMs(policy.maxDelay) : 60_000,
784
- };
785
+ }
785
786
  }
786
787
 
787
788
  function backoffDelay(policy: ResolvedRetryPolicy, attempt: number): number {
788
789
  // `attempt` is 1-indexed; delay applies *before* the next attempt.
789
- if (policy.backoff === "fixed") return Math.min(policy.initial, policy.maxDelay);
790
- if (policy.backoff === "linear") return Math.min(policy.initial * attempt, policy.maxDelay);
790
+ if (policy.backoff === "fixed") return Math.min(policy.initial, policy.maxDelay)
791
+ if (policy.backoff === "linear") return Math.min(policy.initial * attempt, policy.maxDelay)
791
792
  // exponential
792
- return Math.min(policy.initial * Math.pow(2, attempt - 1), policy.maxDelay);
793
+ return Math.min(policy.initial * 2 ** (attempt - 1), policy.maxDelay)
793
794
  }
794
795
 
795
796
  function readRetryAfter(err: SerializedError | undefined): number | undefined {
796
- if (!err) return undefined;
797
- if (err.code !== "RETRYABLE") return undefined;
798
- const raw = (err.data as { retryAfter?: unknown } | undefined)?.retryAfter;
799
- if (raw === undefined) return undefined;
800
- if (typeof raw === "number") return raw;
801
- if (raw instanceof Date) return raw.getTime() - Date.now();
797
+ if (!err) return undefined
798
+ if (err.code !== "RETRYABLE") return undefined
799
+ const raw = (err.data as { retryAfter?: unknown } | undefined)?.retryAfter
800
+ if (raw === undefined) return undefined
801
+ if (typeof raw === "number") return raw
802
+ if (raw instanceof Date) return raw.getTime() - Date.now()
802
803
  if (typeof raw === "string") {
803
- try { return toMs(raw as Duration); } catch { return undefined; }
804
+ try {
805
+ return toMs(raw as Duration)
806
+ } catch {
807
+ return undefined
808
+ }
804
809
  }
805
- return undefined;
810
+ return undefined
806
811
  }
807
812
 
808
813
  /**
@@ -812,27 +817,34 @@ function readRetryAfter(err: SerializedError | undefined): number | undefined {
812
817
  * through `setTimeout(0)` at most) so the suite stays fast.
813
818
  */
814
819
  async function maybeDelay(ms: number): Promise<void> {
815
- if (ms <= 0) return;
820
+ if (ms <= 0) return
816
821
  // Cap at 10ms in-process regardless of declared delay. Test harness
817
822
  // doesn't model real time; production replaces this with a DO alarm.
818
- await new Promise((resolve) => setTimeout(resolve, Math.min(ms, 10)));
823
+ await new Promise((resolve) => setTimeout(resolve, Math.min(ms, 10)))
819
824
  }
820
825
 
821
826
  function toMs(d: Duration): number {
822
- if (typeof d === "number") return d;
823
- const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(d);
824
- if (!m) throw new Error(`invalid duration: ${String(d)}`);
825
- const n = Number(m[1]);
827
+ if (typeof d === "number") return d
828
+ const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(d)
829
+ if (!m) throw new Error(`invalid duration: ${String(d)}`)
830
+ const n = Number(m[1])
826
831
  switch (m[2]) {
827
- case "ms": return n;
828
- case "s": return n * 1000;
829
- case "m": return n * 60_000;
830
- case "h": return n * 3_600_000;
831
- case "d": return n * 86_400_000;
832
- case "w": return n * 604_800_000;
833
- default: throw new Error(`invalid duration unit: ${m[2]}`);
832
+ case "ms":
833
+ return n
834
+ case "s":
835
+ return n * 1000
836
+ case "m":
837
+ return n * 60_000
838
+ case "h":
839
+ return n * 3_600_000
840
+ case "d":
841
+ return n * 86_400_000
842
+ case "w":
843
+ return n * 604_800_000
844
+ default:
845
+ throw new Error(`invalid duration unit: ${m[2]}`)
834
846
  }
835
847
  }
836
848
 
837
849
  // Re-exports used by the executor for metadata type checking.
838
- export type { MetadataValue };
850
+ export type { MetadataValue }