@voyant-travel/workflows 0.107.10

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 (120) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +52 -0
  3. package/README.md +79 -0
  4. package/dist/auth/index.d.ts +125 -0
  5. package/dist/auth/index.d.ts.map +1 -0
  6. package/dist/auth/index.js +352 -0
  7. package/dist/bindings.d.ts +119 -0
  8. package/dist/bindings.d.ts.map +1 -0
  9. package/dist/bindings.js +19 -0
  10. package/dist/client.d.ts +135 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +305 -0
  13. package/dist/conditions.d.ts +29 -0
  14. package/dist/conditions.d.ts.map +1 -0
  15. package/dist/conditions.js +5 -0
  16. package/dist/config.d.ts +93 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +7 -0
  19. package/dist/driver.d.ts +237 -0
  20. package/dist/driver.d.ts.map +1 -0
  21. package/dist/driver.js +53 -0
  22. package/dist/errors.d.ts +58 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +76 -0
  25. package/dist/events/compile.d.ts +34 -0
  26. package/dist/events/compile.d.ts.map +1 -0
  27. package/dist/events/compile.js +204 -0
  28. package/dist/events/index.d.ts +8 -0
  29. package/dist/events/index.d.ts.map +1 -0
  30. package/dist/events/index.js +11 -0
  31. package/dist/events/input-mapper.d.ts +24 -0
  32. package/dist/events/input-mapper.d.ts.map +1 -0
  33. package/dist/events/input-mapper.js +169 -0
  34. package/dist/events/manifest-builder.d.ts +42 -0
  35. package/dist/events/manifest-builder.d.ts.map +1 -0
  36. package/dist/events/manifest-builder.js +313 -0
  37. package/dist/events/payload-hash.d.ts +46 -0
  38. package/dist/events/payload-hash.d.ts.map +1 -0
  39. package/dist/events/payload-hash.js +98 -0
  40. package/dist/events/predicate.d.ts +77 -0
  41. package/dist/events/predicate.d.ts.map +1 -0
  42. package/dist/events/predicate.js +347 -0
  43. package/dist/events/registry.d.ts +37 -0
  44. package/dist/events/registry.d.ts.map +1 -0
  45. package/dist/events/registry.js +47 -0
  46. package/dist/handler/index.d.ts +114 -0
  47. package/dist/handler/index.d.ts.map +1 -0
  48. package/dist/handler/index.js +267 -0
  49. package/dist/handler/resume.d.ts +41 -0
  50. package/dist/handler/resume.d.ts.map +1 -0
  51. package/dist/handler/resume.js +44 -0
  52. package/dist/http-ingest.d.ts +54 -0
  53. package/dist/http-ingest.d.ts.map +1 -0
  54. package/dist/http-ingest.js +214 -0
  55. package/dist/index.d.ts +6 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +10 -0
  58. package/dist/protocol/index.d.ts +345 -0
  59. package/dist/protocol/index.d.ts.map +1 -0
  60. package/dist/protocol/index.js +110 -0
  61. package/dist/rate-limit/index.d.ts +40 -0
  62. package/dist/rate-limit/index.d.ts.map +1 -0
  63. package/dist/rate-limit/index.js +139 -0
  64. package/dist/runtime/ctx.d.ts +111 -0
  65. package/dist/runtime/ctx.d.ts.map +1 -0
  66. package/dist/runtime/ctx.js +624 -0
  67. package/dist/runtime/determinism.d.ts +19 -0
  68. package/dist/runtime/determinism.d.ts.map +1 -0
  69. package/dist/runtime/determinism.js +61 -0
  70. package/dist/runtime/errors.d.ts +21 -0
  71. package/dist/runtime/errors.d.ts.map +1 -0
  72. package/dist/runtime/errors.js +45 -0
  73. package/dist/runtime/executor.d.ts +166 -0
  74. package/dist/runtime/executor.d.ts.map +1 -0
  75. package/dist/runtime/executor.js +226 -0
  76. package/dist/runtime/journal.d.ts +56 -0
  77. package/dist/runtime/journal.d.ts.map +1 -0
  78. package/dist/runtime/journal.js +28 -0
  79. package/dist/testing/index.d.ts +117 -0
  80. package/dist/testing/index.d.ts.map +1 -0
  81. package/dist/testing/index.js +599 -0
  82. package/dist/trigger.d.ts +37 -0
  83. package/dist/trigger.d.ts.map +1 -0
  84. package/dist/trigger.js +11 -0
  85. package/dist/types.d.ts +63 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +3 -0
  88. package/dist/workflow.d.ts +222 -0
  89. package/dist/workflow.d.ts.map +1 -0
  90. package/dist/workflow.js +55 -0
  91. package/package.json +120 -0
  92. package/src/auth/index.ts +398 -0
  93. package/src/bindings.ts +135 -0
  94. package/src/client.ts +498 -0
  95. package/src/conditions.ts +43 -0
  96. package/src/config.ts +114 -0
  97. package/src/driver.ts +277 -0
  98. package/src/errors.ts +109 -0
  99. package/src/events/compile.ts +268 -0
  100. package/src/events/index.ts +42 -0
  101. package/src/events/input-mapper.ts +201 -0
  102. package/src/events/manifest-builder.ts +372 -0
  103. package/src/events/payload-hash.ts +110 -0
  104. package/src/events/predicate.ts +390 -0
  105. package/src/events/registry.ts +86 -0
  106. package/src/handler/index.ts +413 -0
  107. package/src/handler/resume.ts +100 -0
  108. package/src/http-ingest.ts +299 -0
  109. package/src/index.ts +18 -0
  110. package/src/protocol/index.ts +483 -0
  111. package/src/rate-limit/index.ts +181 -0
  112. package/src/runtime/ctx.ts +876 -0
  113. package/src/runtime/determinism.ts +75 -0
  114. package/src/runtime/errors.ts +58 -0
  115. package/src/runtime/executor.ts +442 -0
  116. package/src/runtime/journal.ts +80 -0
  117. package/src/testing/index.ts +796 -0
  118. package/src/trigger.ts +63 -0
  119. package/src/types.ts +80 -0
  120. package/src/workflow.ts +328 -0
@@ -0,0 +1,624 @@
1
+ // Builds the `ctx` object passed to the workflow body.
2
+ // agent-quality: file-size exception -- Central ctx API wiring remains together until wait/stream/group runtime slices can be extracted safely.
3
+ //
4
+ // The executor owns the waitpoint-pending queue and the callbacks
5
+ // into the orchestrator; ctx is a thin shell that delegates.
6
+ import { advanceClockTo, createRandomUUID, now } from "./determinism.js";
7
+ import { CompensateRequestedSignal, isCompensateRequested, isRunCancelled, isWaitpointPending, RunCancelledSignal, WaitpointPendingSignal, } from "./errors.js";
8
+ /**
9
+ * Default resolver used when no container is plumbed through. Throws on
10
+ * `resolve(...)` so failures are visible at the call site instead of
11
+ * returning undefined; `has(...)` returns `false` so optional-dep
12
+ * patterns work cleanly.
13
+ */
14
+ const NO_OP_SERVICE_RESOLVER = {
15
+ resolve(name) {
16
+ throw new Error(`ctx.services.resolve("${name}"): no service container is wired into this workflow runtime. ` +
17
+ `Pass { services } to the driver factory (e.g. via createApp({ workflows: { driver } }))`);
18
+ },
19
+ has() {
20
+ return false;
21
+ },
22
+ };
23
+ export function buildCtx(args) {
24
+ const { env, journal, callbacks, clock, random, retryOverride } = args;
25
+ const services = args.services ?? NO_OP_SERVICE_RESOLVER;
26
+ // Per-ctx client-id counter. Reset on each ctx (= each invocation),
27
+ // which means ids are stable relative to body execution order.
28
+ let clientIdSeq = 0;
29
+ const nextClientId = () => ++clientIdSeq;
30
+ function checkCancel() {
31
+ if (callbacks.abortSignal.aborted) {
32
+ throw new RunCancelledSignal();
33
+ }
34
+ }
35
+ // ---- step ----
36
+ const step = (async (id, optsOrFn, maybeFn) => {
37
+ checkCancel();
38
+ const opts = typeof optsOrFn === "function" ? {} : optsOrFn;
39
+ const fn = typeof optsOrFn === "function" ? optsOrFn : maybeFn;
40
+ // Journal hit? Return cached.
41
+ const cached = journal.stepResults[id];
42
+ if (cached) {
43
+ advanceClockTo(clock, cached.finishedAt);
44
+ if (cached.status === "ok") {
45
+ // Re-register compensable on replay so compensations are available
46
+ // if this invocation ends up rolling back.
47
+ if (opts.compensate) {
48
+ callbacks.recordCompensable({
49
+ stepId: id,
50
+ output: cached.output,
51
+ compensate: opts.compensate,
52
+ });
53
+ }
54
+ return cached.output;
55
+ }
56
+ // Journaled error rethrows on replay so catch blocks behave consistently.
57
+ const e = new Error(cached.error?.message ?? "step failed");
58
+ e.code = cached.error?.code;
59
+ throw e;
60
+ }
61
+ // Execute a new step via the callback, with the retry loop.
62
+ const mergedOpts = {
63
+ ...opts,
64
+ retry: opts.retry ?? retryOverride.current,
65
+ };
66
+ const policy = normalizeRetry(mergedOpts.retry);
67
+ let attempt = 0;
68
+ let lastEntry;
69
+ // Per-step timeout: compose the run-level abort signal with a
70
+ // per-call AbortSignal.timeout so cooperative step bodies (fetch,
71
+ // setTimeout wrappers, custom AbortSignal observers) stop early
72
+ // on timeout. Hard enforcement for uncooperative bodies is done
73
+ // below by racing the wrapped fn against a timeout rejection.
74
+ const timeoutMs = mergedOpts.timeout !== undefined ? toMs(mergedOpts.timeout) : undefined;
75
+ const fnWithTimeout = timeoutMs !== undefined
76
+ ? async (stepCtx) => {
77
+ let timer;
78
+ try {
79
+ return await Promise.race([
80
+ fn(stepCtx),
81
+ new Promise((_, reject) => {
82
+ timer = setTimeout(() => {
83
+ const e = new Error(`step "${id}" timed out after ${timeoutMs}ms`);
84
+ e.code = "TIMEOUT";
85
+ reject(e);
86
+ }, timeoutMs);
87
+ }),
88
+ ]);
89
+ }
90
+ finally {
91
+ if (timer !== undefined)
92
+ clearTimeout(timer);
93
+ }
94
+ }
95
+ : fn;
96
+ while (attempt < policy.max) {
97
+ attempt += 1;
98
+ const stepCtx = {
99
+ signal: timeoutMs !== undefined
100
+ ? AbortSignal.any([callbacks.abortSignal, AbortSignal.timeout(timeoutMs)])
101
+ : callbacks.abortSignal,
102
+ attempt,
103
+ log: (level, msg, data) => {
104
+ console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](`[${id}]`, msg, data ?? "");
105
+ },
106
+ };
107
+ const entry = await callbacks.runStep({
108
+ stepId: id,
109
+ attempt,
110
+ input: undefined,
111
+ options: mergedOpts,
112
+ fn: fnWithTimeout,
113
+ stepCtx,
114
+ });
115
+ lastEntry = entry;
116
+ if (entry.status === "ok") {
117
+ journal.stepResults[id] = entry;
118
+ advanceClockTo(clock, entry.finishedAt);
119
+ if (opts.compensate) {
120
+ callbacks.recordCompensable({
121
+ stepId: id,
122
+ output: entry.output,
123
+ compensate: opts.compensate,
124
+ });
125
+ }
126
+ return entry.output;
127
+ }
128
+ // Failed attempt. Check if we should stop retrying.
129
+ if (entry.error?.code === "FATAL")
130
+ break;
131
+ if (attempt >= policy.max)
132
+ break;
133
+ // In production the step handler returns { retryAfter } to the DO
134
+ // which sets an alarm; here the spike/test harness continues
135
+ // immediately. retryAfter from RetryableError wins over the policy
136
+ // backoff when set.
137
+ const retryAfter = readRetryAfter(entry.error);
138
+ await maybeDelay(retryAfter ?? backoffDelay(policy, attempt));
139
+ }
140
+ // Retries exhausted (or never retried).
141
+ const finalEntry = lastEntry;
142
+ journal.stepResults[id] = finalEntry;
143
+ advanceClockTo(clock, finalEntry.finishedAt);
144
+ const e = new Error(finalEntry.error?.message ?? "step failed");
145
+ e.code = finalEntry.error?.code;
146
+ throw e;
147
+ });
148
+ // ---- waits ----
149
+ function yieldWaitpoint(clientWaitpointId, kind, meta, timeoutMs) {
150
+ callbacks.registerWaitpoint({ clientWaitpointId, kind, meta, timeoutMs });
151
+ throw new WaitpointPendingSignal(clientWaitpointId);
152
+ }
153
+ function lookupWaitpoint(id) {
154
+ return journal.waitpointsResolved[id];
155
+ }
156
+ const sleep = async (duration) => {
157
+ checkCancel();
158
+ const id = `sleep:${nextClientId()}`;
159
+ const resolved = lookupWaitpoint(id);
160
+ if (resolved) {
161
+ advanceClockTo(clock, resolved.resolvedAt);
162
+ return;
163
+ }
164
+ const ms = toMs(duration);
165
+ yieldWaitpoint(id, "DATETIME", { durationMs: ms }, ms);
166
+ };
167
+ function makeWaitable(kind, clientWaitpointId, iterIdPrefix, meta, timeoutMs, onTimeout = "null") {
168
+ // --- thenable: single first-match-wins resolution ---
169
+ const resolve = () => {
170
+ const resolved = lookupWaitpoint(clientWaitpointId);
171
+ if (!resolved) {
172
+ yieldWaitpoint(clientWaitpointId, kind, meta, timeoutMs);
173
+ }
174
+ advanceClockTo(clock, resolved.resolvedAt);
175
+ if (resolved.payload === undefined && onTimeout === "throw") {
176
+ throw new Error(`waitpoint ${clientWaitpointId} timed out`);
177
+ }
178
+ return (resolved.payload ?? null);
179
+ };
180
+ // --- iterable: fresh waitpoint per .next() call ---
181
+ function makeIterator() {
182
+ let closed = false;
183
+ return {
184
+ async next() {
185
+ if (closed)
186
+ return { value: undefined, done: true };
187
+ checkCancel();
188
+ const iterId = `${iterIdPrefix}:iter:${nextClientId()}`;
189
+ const resolvedIter = lookupWaitpoint(iterId);
190
+ if (!resolvedIter) {
191
+ yieldWaitpoint(iterId, kind, { ...meta, iter: true }, timeoutMs);
192
+ }
193
+ advanceClockTo(clock, resolvedIter.resolvedAt);
194
+ // End-of-stream marker. Harness / orchestrator writes this to
195
+ // tell the iterator the source has no more events.
196
+ const payload = resolvedIter.payload;
197
+ if (isStreamEnd(payload)) {
198
+ closed = true;
199
+ return { value: undefined, done: true };
200
+ }
201
+ if (payload === undefined && onTimeout === "throw") {
202
+ throw new Error(`waitpoint ${iterId} timed out`);
203
+ }
204
+ return { value: payload, done: false };
205
+ },
206
+ async return() {
207
+ closed = true;
208
+ return { value: undefined, done: true };
209
+ },
210
+ [Symbol.asyncIterator]() {
211
+ return this;
212
+ },
213
+ };
214
+ }
215
+ const thenable = {
216
+ // biome-ignore lint/suspicious/noThenProperty: Waitable intentionally implements PromiseLike for `await`.
217
+ then(onFulfilled, onRejected) {
218
+ try {
219
+ const r = resolve();
220
+ return Promise.resolve(r).then(onFulfilled, onRejected);
221
+ }
222
+ catch (e) {
223
+ return Promise.reject(e).then(onFulfilled, onRejected);
224
+ }
225
+ },
226
+ [Symbol.asyncIterator]() {
227
+ return makeIterator();
228
+ },
229
+ close() {
230
+ // no-op; `return()` on the iterator handles early break.
231
+ },
232
+ };
233
+ return thenable;
234
+ }
235
+ function isStreamEnd(payload) {
236
+ return (typeof payload === "object" &&
237
+ payload !== null &&
238
+ payload.__voyantStreamEnd === true);
239
+ }
240
+ const waitForEvent = ((eventType, opts) => {
241
+ checkCancel();
242
+ const thenableId = `event:${eventType}:${nextClientId()}`;
243
+ const iterPrefix = `event:${eventType}`;
244
+ return makeWaitable("EVENT", thenableId, iterPrefix, { eventType }, opts?.timeout ? toMs(opts.timeout) : undefined, opts?.onTimeout);
245
+ });
246
+ const waitForSignal = ((name, opts) => {
247
+ checkCancel();
248
+ const thenableId = `signal:${name}:${nextClientId()}`;
249
+ const iterPrefix = `signal:${name}`;
250
+ return makeWaitable("SIGNAL", thenableId, iterPrefix, { signalName: name }, opts?.timeout ? toMs(opts.timeout) : undefined, opts?.onTimeout);
251
+ });
252
+ const waitForToken = (async (opts) => {
253
+ checkCancel();
254
+ // Allocate a stable id per call. User-supplied `tokenId` is kept
255
+ // verbatim so external systems can reference the same value.
256
+ const tokenId = opts?.tokenId ?? `tok_${nextClientId()}`;
257
+ const waitpointId = `token:${tokenId}`;
258
+ const timeoutMs = opts?.timeout ? toMs(opts.timeout) : undefined;
259
+ const onTimeout = opts?.onTimeout ?? "null";
260
+ return {
261
+ tokenId,
262
+ url: `/__voyant/tokens/${tokenId}`,
263
+ wait: async () => {
264
+ checkCancel();
265
+ const resolved = lookupWaitpoint(waitpointId);
266
+ if (resolved) {
267
+ advanceClockTo(clock, resolved.resolvedAt);
268
+ if (resolved.payload === undefined && onTimeout === "throw") {
269
+ throw new Error(`token ${tokenId} timed out`);
270
+ }
271
+ return resolved.payload ?? null;
272
+ }
273
+ yieldWaitpoint(waitpointId, "MANUAL", { tokenId }, timeoutMs);
274
+ },
275
+ };
276
+ });
277
+ // ---- invoke / parallel ----
278
+ const invoke = (async (wf, input, opts) => {
279
+ checkCancel();
280
+ const id = `invoke:${wf.id}:${nextClientId()}`;
281
+ const resolved = journal.waitpointsResolved[id];
282
+ if (resolved) {
283
+ advanceClockTo(clock, resolved.resolvedAt);
284
+ if (resolved.error) {
285
+ const e = new Error(resolved.error.message);
286
+ e.code = resolved.error.code;
287
+ throw e;
288
+ }
289
+ return resolved.payload;
290
+ }
291
+ yieldWaitpoint(id, "RUN", {
292
+ childWorkflowId: wf.id,
293
+ childInput: input,
294
+ detach: opts?.detach ?? false,
295
+ tags: opts?.tags ?? [],
296
+ lockToVersion: opts?.lockToVersion,
297
+ idempotencyKey: opts?.idempotencyKey,
298
+ });
299
+ });
300
+ const parallel = async (items, fn, opts) => {
301
+ checkCancel();
302
+ const total = items.length;
303
+ if (total === 0)
304
+ return [];
305
+ const concurrency = Math.max(1, opts?.concurrency ?? total);
306
+ const settle = opts?.settle ?? false;
307
+ const results = new Array(total);
308
+ const errors = [];
309
+ let cursor = 0;
310
+ let aborted = false;
311
+ async function worker() {
312
+ while (!aborted) {
313
+ const i = cursor++;
314
+ if (i >= total)
315
+ return;
316
+ try {
317
+ results[i] = await fn(items[i], i);
318
+ }
319
+ catch (err) {
320
+ if (settle) {
321
+ errors.push({ index: i, error: err });
322
+ }
323
+ else {
324
+ aborted = true;
325
+ throw err;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ const workerCount = Math.min(concurrency, total);
331
+ const workers = Array.from({ length: workerCount }, () => worker());
332
+ if (settle) {
333
+ await Promise.all(workers);
334
+ if (errors.length > 0) {
335
+ // Attach details so callers can inspect which items failed.
336
+ const agg = new AggregateError(errors.map((e) => (e.error instanceof Error ? e.error : new Error(String(e.error)))), `ctx.parallel: ${errors.length}/${total} iteration${errors.length === 1 ? "" : "s"} failed`);
337
+ agg.failedIndices = errors.map((e) => e.index);
338
+ throw agg;
339
+ }
340
+ return results;
341
+ }
342
+ await Promise.all(workers);
343
+ return results;
344
+ };
345
+ // ---- streams ----
346
+ const activeStreamIds = new Set();
347
+ async function consumeStream(streamId, source, encoding) {
348
+ checkCancel();
349
+ if (activeStreamIds.has(streamId)) {
350
+ throw new Error(`ctx.stream: duplicate streamId "${streamId}" within the same run`);
351
+ }
352
+ activeStreamIds.add(streamId);
353
+ // Replay skip: the prior invocation already drained this source
354
+ // and the orchestrator has the chunks. Re-iterating would double
355
+ // any side effects (LLM calls, billable APIs, file reads).
356
+ if (journal.streamsCompleted[streamId]) {
357
+ return;
358
+ }
359
+ let seq = 0;
360
+ const iter = source[Symbol.asyncIterator]();
361
+ try {
362
+ while (true) {
363
+ checkCancel();
364
+ const { value, done } = await iter.next();
365
+ if (done) {
366
+ callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true });
367
+ journal.streamsCompleted[streamId] = { chunkCount: seq + 1 };
368
+ return;
369
+ }
370
+ seq += 1;
371
+ const chunk = normalizeChunk(value, encoding);
372
+ callbacks.pushStreamChunk({ streamId, seq, encoding, chunk, final: false });
373
+ }
374
+ }
375
+ catch (err) {
376
+ // Emit a final frame so consumers know the stream closed, then
377
+ // propagate so the workflow body's error handling kicks in. No
378
+ // journal entry — a failed stream should re-iterate on replay
379
+ // (so the error surfaces deterministically).
380
+ callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true });
381
+ throw err;
382
+ }
383
+ }
384
+ const streamImpl = async (streamId, sourceFn) => {
385
+ const source = sourceFn();
386
+ await consumeStream(streamId, source, inferEncoding(source));
387
+ };
388
+ const stream = Object.assign(streamImpl, {
389
+ text: async (id, source) => {
390
+ await consumeStream(id, source, "text");
391
+ },
392
+ json: async (id, source) => {
393
+ await consumeStream(id, source, "json");
394
+ },
395
+ bytes: async (id, source) => {
396
+ await consumeStream(id, source, "base64");
397
+ },
398
+ });
399
+ // ---- groups ----
400
+ // `ctx.group(name, fn)` creates a compensation scope. Implementation
401
+ // strategy: the outer compensable list is the single source of truth;
402
+ // each group tracks a checkpoint index. If the scope's body throws or
403
+ // explicitly calls `scope.compensate()`, we splice off compensables
404
+ // added since the checkpoint and run them LIFO, leaving outer
405
+ // compensables untouched.
406
+ //
407
+ // If the scope body completes normally, compensables stay in the
408
+ // outer list — they'll still be rolled back if the enclosing workflow
409
+ // later throws.
410
+ const runScopedCompensations = async (fromIndex) => {
411
+ const scopeEntries = callbacks.spliceCompensable(fromIndex);
412
+ for (let i = scopeEntries.length - 1; i >= 0; i--) {
413
+ const c = scopeEntries[i];
414
+ try {
415
+ await c.compensate(c.output);
416
+ }
417
+ catch {
418
+ // One bad compensation in a scope does not abort the others.
419
+ // Errors here don't surface to the executor — the outer rollback
420
+ // machinery only sees the user error that triggered the scope
421
+ // unwind.
422
+ }
423
+ }
424
+ };
425
+ const group = async (_name, fn) => {
426
+ checkCancel();
427
+ const checkpointStart = callbacks.compensableLength();
428
+ try {
429
+ return await fn({
430
+ step,
431
+ compensate: async () => {
432
+ await runScopedCompensations(checkpointStart);
433
+ throw new CompensateRequestedSignal();
434
+ },
435
+ });
436
+ }
437
+ catch (err) {
438
+ // Only run scoped compensations for real user errors — internal
439
+ // signals (waitpoint yield, cancellation, compensate-requested)
440
+ // are re-thrown unchanged so the executor can route them.
441
+ if (!isWaitpointPending(err) && !isRunCancelled(err) && !isCompensateRequested(err)) {
442
+ await runScopedCompensations(checkpointStart);
443
+ }
444
+ throw err;
445
+ }
446
+ };
447
+ // ---- metadata ----
448
+ const metadata = {
449
+ set(key, value) {
450
+ callbacks.pushMetadata({ op: "set", key, value });
451
+ },
452
+ increment(key, by = 1) {
453
+ callbacks.pushMetadata({ op: "increment", key, value: by });
454
+ },
455
+ append(key, value) {
456
+ callbacks.pushMetadata({ op: "append", key, value });
457
+ },
458
+ remove(key) {
459
+ callbacks.pushMetadata({ op: "remove", key });
460
+ },
461
+ // Mutations are pushed immediately via `callbacks.pushMetadata`
462
+ // and collected on the response envelope; no explicit flush is
463
+ // needed.
464
+ flush: async () => { },
465
+ };
466
+ // ---- retry override ----
467
+ function setRetry(policy) {
468
+ retryOverride.current = policy;
469
+ }
470
+ return {
471
+ run: env.run,
472
+ workflow: env.workflow,
473
+ environment: env.environment,
474
+ project: env.project,
475
+ organization: env.organization,
476
+ invocationCount: callbacks.invocationCount,
477
+ signal: callbacks.abortSignal,
478
+ services,
479
+ step,
480
+ sleep,
481
+ waitForEvent,
482
+ waitForSignal,
483
+ waitForToken,
484
+ invoke,
485
+ parallel,
486
+ stream,
487
+ group,
488
+ metadata,
489
+ now: () => now(clock),
490
+ random,
491
+ randomUUID: createRandomUUID(random),
492
+ setRetry,
493
+ compensate: async () => {
494
+ checkCancel();
495
+ throw new CompensateRequestedSignal();
496
+ },
497
+ };
498
+ }
499
+ // ---- helpers ----
500
+ function inferEncoding(source) {
501
+ // Default to json for the generic ctx.stream(id, generator) call. The
502
+ // typed variants (text/json/bytes) override this.
503
+ void source;
504
+ return "json";
505
+ }
506
+ function normalizeChunk(value, encoding) {
507
+ if (encoding === "text") {
508
+ return typeof value === "string" ? value : String(value);
509
+ }
510
+ if (encoding === "base64") {
511
+ if (value instanceof Uint8Array) {
512
+ return toBase64(value);
513
+ }
514
+ throw new Error("ctx.stream.bytes: expected Uint8Array chunks");
515
+ }
516
+ return value; // json — pass through
517
+ }
518
+ function toBase64(bytes) {
519
+ // Node + modern runtimes provide Buffer or btoa. Use Buffer when
520
+ // available for efficiency; fall back to manual encode for isolates.
521
+ const g = globalThis;
522
+ if (g.Buffer)
523
+ return g.Buffer.from(bytes).toString("base64");
524
+ if (g.btoa) {
525
+ let s = "";
526
+ for (let i = 0; i < bytes.length; i++)
527
+ s += String.fromCharCode(bytes[i]);
528
+ return g.btoa(s);
529
+ }
530
+ // Manual fallback (rare).
531
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
532
+ let out = "";
533
+ let i = 0;
534
+ while (i < bytes.length) {
535
+ const b1 = bytes[i++];
536
+ const b2 = i < bytes.length ? bytes[i++] : 0;
537
+ const b3 = i < bytes.length ? bytes[i++] : 0;
538
+ out += chars[b1 >> 2];
539
+ out += chars[((b1 & 3) << 4) | (b2 >> 4)];
540
+ out += i - 1 > bytes.length ? "=" : chars[((b2 & 15) << 2) | (b3 >> 6)];
541
+ out += i > bytes.length ? "=" : chars[b3 & 63];
542
+ }
543
+ return out;
544
+ }
545
+ function normalizeRetry(input) {
546
+ if (!input)
547
+ return { max: 1, backoff: "exponential", initial: 1000, maxDelay: 60_000 };
548
+ const max = input.max ?? 3;
549
+ const policy = input;
550
+ return {
551
+ max: Math.max(1, max),
552
+ backoff: policy.backoff ?? "exponential",
553
+ initial: policy.initial !== undefined ? toMs(policy.initial) : 1000,
554
+ maxDelay: policy.maxDelay !== undefined ? toMs(policy.maxDelay) : 60_000,
555
+ };
556
+ }
557
+ function backoffDelay(policy, attempt) {
558
+ // `attempt` is 1-indexed; delay applies *before* the next attempt.
559
+ if (policy.backoff === "fixed")
560
+ return Math.min(policy.initial, policy.maxDelay);
561
+ if (policy.backoff === "linear")
562
+ return Math.min(policy.initial * attempt, policy.maxDelay);
563
+ // exponential
564
+ return Math.min(policy.initial * 2 ** (attempt - 1), policy.maxDelay);
565
+ }
566
+ function readRetryAfter(err) {
567
+ if (!err)
568
+ return undefined;
569
+ if (err.code !== "RETRYABLE")
570
+ return undefined;
571
+ const raw = err.data?.retryAfter;
572
+ if (raw === undefined)
573
+ return undefined;
574
+ if (typeof raw === "number")
575
+ return raw;
576
+ if (raw instanceof Date)
577
+ return raw.getTime() - Date.now();
578
+ if (typeof raw === "string") {
579
+ try {
580
+ return toMs(raw);
581
+ }
582
+ catch {
583
+ return undefined;
584
+ }
585
+ }
586
+ return undefined;
587
+ }
588
+ /**
589
+ * In the real runtime, retry delay is expressed to the orchestrator as a
590
+ * `retryAfter` field on the step callback response, and the DO sets an
591
+ * alarm — no worker sits idle. In tests we skip the delay (pass it
592
+ * through `setTimeout(0)` at most) so the suite stays fast.
593
+ */
594
+ async function maybeDelay(ms) {
595
+ if (ms <= 0)
596
+ return;
597
+ // Cap at 10ms in-process regardless of declared delay. Test harness
598
+ // doesn't model real time; production replaces this with a DO alarm.
599
+ await new Promise((resolve) => setTimeout(resolve, Math.min(ms, 10)));
600
+ }
601
+ function toMs(d) {
602
+ if (typeof d === "number")
603
+ return d;
604
+ const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(d);
605
+ if (!m)
606
+ throw new Error(`invalid duration: ${String(d)}`);
607
+ const n = Number(m[1]);
608
+ switch (m[2]) {
609
+ case "ms":
610
+ return n;
611
+ case "s":
612
+ return n * 1000;
613
+ case "m":
614
+ return n * 60_000;
615
+ case "h":
616
+ return n * 3_600_000;
617
+ case "d":
618
+ return n * 86_400_000;
619
+ case "w":
620
+ return n * 604_800_000;
621
+ default:
622
+ throw new Error(`invalid duration unit: ${m[2]}`);
623
+ }
624
+ }
@@ -0,0 +1,19 @@
1
+ export interface ClockState {
2
+ /** Base wall-clock time recorded at run start. */
3
+ readonly baseWallClock: number;
4
+ /** Offset from baseWallClock at which ctx.now() should return — set by the executor when replaying journaled events. */
5
+ offset: number;
6
+ }
7
+ export declare function createClock(runStartedAt: number): ClockState;
8
+ export declare function now(clock: ClockState): number;
9
+ /** Advance the clock to the event currently being replayed. */
10
+ export declare function advanceClockTo(clock: ClockState, eventAt: number): void;
11
+ /**
12
+ * Mulberry32 PRNG — fast, fine for workflow-determinism use. Seeded
13
+ * from a 32-bit hash of the run id. Not cryptographic.
14
+ */
15
+ export declare function seededRandom(seed: number): () => number;
16
+ export declare function hashSeed(runId: string): number;
17
+ export declare function createRandom(runId: string): () => number;
18
+ export declare function createRandomUUID(rng: () => number): () => string;
19
+ //# sourceMappingURL=determinism.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"determinism.d.ts","sourceRoot":"","sources":["../../src/runtime/determinism.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,wHAAwH;IACxH,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,UAAU,CAE5D;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAE7C;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CASvD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,MAAM,CAExD;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,MAAM,MAAM,CAgBhE"}