@voyantjs/workflows 0.0.0 → 0.6.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.
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,595 @@
1
+ // @voyantjs/workflows/testing
2
+ //
3
+ // In-process test harness with mocked steps, waits, and invokes.
4
+ // Contract in docs/sdk-surface.md §11.
5
+ //
6
+ // Drives `executeWorkflowStep` across resumptions. Steps resolve
7
+ // from a user-supplied stub map; waitpoints resolve from fixtures.
8
+ import { executeWorkflowStep, } from "../runtime/executor.js";
9
+ import { emptyJournal } from "../runtime/journal.js";
10
+ import { getWorkflow } from "../workflow.js";
11
+ export async function runWorkflowForTest(workflow, input, opts = {}) {
12
+ const def = workflow;
13
+ const now = opts.now ?? (() => Date.now());
14
+ const startedAt = now();
15
+ const journal = emptyJournal();
16
+ const metadata = {};
17
+ const events = [];
18
+ const streams = {};
19
+ const maxInvocations = opts.maxInvocations ?? 16;
20
+ const environment = {
21
+ name: opts.environment?.name ?? "development",
22
+ git: opts.environment?.git,
23
+ };
24
+ let invocationCount = 0;
25
+ let last;
26
+ // Track how many metadata mutations have already been applied so we
27
+ // only apply the delta on each invocation. Otherwise replays
28
+ // double-count `increment` / duplicate `append` values. (Positional
29
+ // dedup; mirrors what the real orchestrator does with journaled ids.)
30
+ let metadataAppliedCount = 0;
31
+ // Per-fixture cursors into iterable event/signal arrays, persisted
32
+ // across invocations so replay doesn't restart consumption.
33
+ const cursors = { event: new Map(), signal: new Map() };
34
+ while (invocationCount < maxInvocations) {
35
+ invocationCount += 1;
36
+ const response = await executeWorkflowStep(def, {
37
+ runId: `run_test_${def.id}`,
38
+ workflowId: def.id,
39
+ workflowVersion: "test",
40
+ input,
41
+ journal,
42
+ invocationCount,
43
+ environment: {
44
+ run: {
45
+ id: `run_test_${def.id}`,
46
+ number: 1,
47
+ attempt: 1,
48
+ triggeredBy: { kind: "api" },
49
+ tags: [],
50
+ startedAt,
51
+ },
52
+ workflow: { id: def.id, version: "test" },
53
+ environment,
54
+ project: { id: "prj_test", slug: "test" },
55
+ organization: { id: "org_test", slug: "test" },
56
+ },
57
+ triggeredBy: { kind: "api" },
58
+ runStartedAt: startedAt,
59
+ tags: [],
60
+ stepRunner: createStepRunner(opts, events, now),
61
+ });
62
+ last = response;
63
+ const newMutations = response.metadataUpdates.slice(metadataAppliedCount);
64
+ applyMetadata(metadata, newMutations);
65
+ metadataAppliedCount = response.metadataUpdates.length;
66
+ for (const chunk of response.streamChunks) {
67
+ const bucket = streams[chunk.streamId] ?? [];
68
+ streams[chunk.streamId] = bucket;
69
+ bucket.push(chunk);
70
+ }
71
+ if (response.status === "completed") {
72
+ return {
73
+ status: "completed",
74
+ output: response.output,
75
+ steps: stepsFromJournal(journal),
76
+ events,
77
+ metadata,
78
+ compensations: [],
79
+ streams,
80
+ invocations: invocationCount,
81
+ };
82
+ }
83
+ if (response.status === "failed") {
84
+ return {
85
+ status: "failed",
86
+ error: {
87
+ category: response.error.category,
88
+ code: response.error.code,
89
+ message: response.error.message,
90
+ },
91
+ steps: stepsFromJournal(journal),
92
+ events,
93
+ metadata,
94
+ compensations: [],
95
+ streams,
96
+ invocations: invocationCount,
97
+ };
98
+ }
99
+ if (response.status === "cancelled") {
100
+ return {
101
+ status: "cancelled",
102
+ steps: stepsFromJournal(journal),
103
+ events,
104
+ metadata,
105
+ compensations: response.compensations,
106
+ streams,
107
+ invocations: invocationCount,
108
+ };
109
+ }
110
+ if (response.status === "compensated" || response.status === "compensation_failed") {
111
+ return {
112
+ status: response.status,
113
+ error: response.error
114
+ ? {
115
+ category: response.error.category,
116
+ code: response.error.code,
117
+ message: response.error.message,
118
+ }
119
+ : undefined,
120
+ steps: stepsFromJournal(journal),
121
+ events,
122
+ metadata,
123
+ compensations: response.compensations,
124
+ streams,
125
+ invocations: invocationCount,
126
+ };
127
+ }
128
+ // Waiting: resolve waitpoints from fixtures and loop. Waitpoints
129
+ // that have no fixture match either throw (default) or, when
130
+ // `pauseOnWait` is set, park the run and return a "waiting" result.
131
+ const stillPending = [];
132
+ for (const wp of response.waitpoints) {
133
+ const resolved = await resolveWaitpoint(wp, opts, now, events, cursors);
134
+ if (!resolved) {
135
+ if (opts.pauseOnWait) {
136
+ stillPending.push(wp);
137
+ continue;
138
+ }
139
+ throw new Error(`test harness: waitpoint ${wp.clientWaitpointId} (${wp.kind}) has no fixture resolution. ` +
140
+ `Provide one via TestOptions.waitForEvent / waitForSignal / (sleeps auto-resolve), ` +
141
+ `or set TestOptions.pauseOnWait to park the run.`);
142
+ }
143
+ journal.waitpointsResolved[wp.clientWaitpointId] = resolved;
144
+ events.push({
145
+ type: `waitpoint.resolved:${wp.kind}`,
146
+ at: resolved.resolvedAt,
147
+ data: resolved.payload ?? null,
148
+ });
149
+ }
150
+ if (stillPending.length > 0) {
151
+ return {
152
+ status: "waiting",
153
+ steps: stepsFromJournal(journal),
154
+ events,
155
+ metadata,
156
+ compensations: [],
157
+ streams,
158
+ invocations: invocationCount,
159
+ pause: {
160
+ journal,
161
+ pendingWaitpoints: stillPending,
162
+ startedAt,
163
+ invocationCount,
164
+ metadataAppliedCount,
165
+ fixtureCursors: {
166
+ event: Object.fromEntries(cursors.event.entries()),
167
+ signal: Object.fromEntries(cursors.signal.entries()),
168
+ },
169
+ },
170
+ };
171
+ }
172
+ }
173
+ throw new Error(`test harness exceeded maxInvocations (${maxInvocations}). ` +
174
+ `Last status: ${last?.status ?? "<none>"}. Possible infinite waitpoint loop.`);
175
+ }
176
+ /**
177
+ * Resume a parked run. Matches the injection against one of the
178
+ * `pause.pendingWaitpoints`, records it in the journal, and re-enters
179
+ * the executor loop until the next pause or terminal state.
180
+ */
181
+ export async function resumeWorkflowForTest(workflow, input, opts) {
182
+ const def = workflow;
183
+ const now = opts.now ?? (() => Date.now());
184
+ const maxInvocations = opts.maxInvocations ?? 16;
185
+ const matched = matchWaitpoint(opts.pause.pendingWaitpoints, opts.injection);
186
+ if (!matched) {
187
+ throw new Error(`resume: no pending waitpoint matches injection kind=${opts.injection.kind}, ` +
188
+ `key=${injectionKey(opts.injection)}`);
189
+ }
190
+ const journal = cloneJournal(opts.pause.journal);
191
+ journal.waitpointsResolved[matched.clientWaitpointId] = {
192
+ kind: matched.kind,
193
+ resolvedAt: now(),
194
+ payload: opts.injection.payload,
195
+ source: "live",
196
+ matchedEventId: opts.injection.kind === "EVENT" ? `evt_live_${opts.injection.eventType}` : undefined,
197
+ };
198
+ const events = [
199
+ {
200
+ type: `waitpoint.resolved:${matched.kind}`,
201
+ at: now(),
202
+ data: opts.injection.payload ?? null,
203
+ },
204
+ ];
205
+ const metadata = {};
206
+ const streams = {};
207
+ const cursors = {
208
+ event: new Map(Object.entries(opts.pause.fixtureCursors.event)),
209
+ signal: new Map(Object.entries(opts.pause.fixtureCursors.signal)),
210
+ };
211
+ const environment = {
212
+ name: opts.environment?.name ?? "development",
213
+ git: opts.environment?.git,
214
+ };
215
+ let invocationCount = opts.pause.invocationCount;
216
+ let metadataAppliedCount = opts.pause.metadataAppliedCount;
217
+ let last;
218
+ // Remaining pending waitpoints from the previous pause (still parked).
219
+ let carryover = opts.pause.pendingWaitpoints.filter((w) => w.clientWaitpointId !== matched.clientWaitpointId);
220
+ while (invocationCount < maxInvocations) {
221
+ invocationCount += 1;
222
+ const response = await executeWorkflowStep(def, {
223
+ runId: `run_test_${def.id}`,
224
+ workflowId: def.id,
225
+ workflowVersion: "test",
226
+ input,
227
+ journal,
228
+ invocationCount,
229
+ environment: {
230
+ run: {
231
+ id: `run_test_${def.id}`,
232
+ number: 1,
233
+ attempt: 1,
234
+ triggeredBy: { kind: "api" },
235
+ tags: [],
236
+ startedAt: opts.pause.startedAt,
237
+ },
238
+ workflow: { id: def.id, version: "test" },
239
+ environment,
240
+ project: { id: "prj_test", slug: "test" },
241
+ organization: { id: "org_test", slug: "test" },
242
+ },
243
+ triggeredBy: { kind: "api" },
244
+ runStartedAt: opts.pause.startedAt,
245
+ tags: [],
246
+ stepRunner: createStepRunner(opts, events, now),
247
+ });
248
+ last = response;
249
+ const newMutations = response.metadataUpdates.slice(metadataAppliedCount);
250
+ applyMetadata(metadata, newMutations);
251
+ metadataAppliedCount = response.metadataUpdates.length;
252
+ for (const chunk of response.streamChunks) {
253
+ const bucket = streams[chunk.streamId] ?? [];
254
+ streams[chunk.streamId] = bucket;
255
+ bucket.push(chunk);
256
+ }
257
+ if (response.status === "completed") {
258
+ return {
259
+ status: "completed",
260
+ output: response.output,
261
+ steps: stepsFromJournal(journal),
262
+ events,
263
+ metadata,
264
+ compensations: [],
265
+ streams,
266
+ invocations: invocationCount,
267
+ };
268
+ }
269
+ if (response.status === "failed") {
270
+ return {
271
+ status: "failed",
272
+ error: {
273
+ category: response.error.category,
274
+ code: response.error.code,
275
+ message: response.error.message,
276
+ },
277
+ steps: stepsFromJournal(journal),
278
+ events,
279
+ metadata,
280
+ compensations: [],
281
+ streams,
282
+ invocations: invocationCount,
283
+ };
284
+ }
285
+ if (response.status === "cancelled") {
286
+ return {
287
+ status: "cancelled",
288
+ steps: stepsFromJournal(journal),
289
+ events,
290
+ metadata,
291
+ compensations: response.compensations,
292
+ streams,
293
+ invocations: invocationCount,
294
+ };
295
+ }
296
+ if (response.status === "compensated" || response.status === "compensation_failed") {
297
+ return {
298
+ status: response.status,
299
+ error: response.error
300
+ ? {
301
+ category: response.error.category,
302
+ code: response.error.code,
303
+ message: response.error.message,
304
+ }
305
+ : undefined,
306
+ steps: stepsFromJournal(journal),
307
+ events,
308
+ metadata,
309
+ compensations: response.compensations,
310
+ streams,
311
+ invocations: invocationCount,
312
+ };
313
+ }
314
+ const stillPending = [...carryover];
315
+ for (const wp of response.waitpoints) {
316
+ const resolved = await resolveWaitpoint(wp, opts, now, events, cursors);
317
+ if (!resolved) {
318
+ if (opts.pauseOnWait) {
319
+ stillPending.push(wp);
320
+ continue;
321
+ }
322
+ throw new Error(`resume: waitpoint ${wp.clientWaitpointId} (${wp.kind}) has no fixture resolution. ` +
323
+ `Provide one via TestOptions.waitForEvent / waitForSignal, or set pauseOnWait.`);
324
+ }
325
+ journal.waitpointsResolved[wp.clientWaitpointId] = resolved;
326
+ events.push({
327
+ type: `waitpoint.resolved:${wp.kind}`,
328
+ at: resolved.resolvedAt,
329
+ data: resolved.payload ?? null,
330
+ });
331
+ }
332
+ carryover = []; // consumed into stillPending
333
+ if (stillPending.length > 0) {
334
+ return {
335
+ status: "waiting",
336
+ steps: stepsFromJournal(journal),
337
+ events,
338
+ metadata,
339
+ compensations: [],
340
+ streams,
341
+ invocations: invocationCount,
342
+ pause: {
343
+ journal,
344
+ pendingWaitpoints: stillPending,
345
+ startedAt: opts.pause.startedAt,
346
+ invocationCount,
347
+ metadataAppliedCount,
348
+ fixtureCursors: {
349
+ event: Object.fromEntries(cursors.event.entries()),
350
+ signal: Object.fromEntries(cursors.signal.entries()),
351
+ },
352
+ },
353
+ };
354
+ }
355
+ }
356
+ throw new Error(`resume: exceeded maxInvocations (${maxInvocations}). Last status: ${last?.status ?? "<none>"}.`);
357
+ }
358
+ function matchWaitpoint(pending, inj) {
359
+ for (const wp of pending) {
360
+ if (wp.kind !== inj.kind)
361
+ continue;
362
+ if (inj.kind === "EVENT" && wp.meta.eventType === inj.eventType)
363
+ return wp;
364
+ if (inj.kind === "SIGNAL" && wp.meta.signalName === inj.name)
365
+ return wp;
366
+ if (inj.kind === "MANUAL" && wp.meta.tokenId === inj.tokenId)
367
+ return wp;
368
+ }
369
+ return undefined;
370
+ }
371
+ function injectionKey(inj) {
372
+ if (inj.kind === "EVENT")
373
+ return inj.eventType;
374
+ if (inj.kind === "SIGNAL")
375
+ return inj.name;
376
+ return inj.tokenId;
377
+ }
378
+ function cloneJournal(j) {
379
+ return {
380
+ stepResults: { ...j.stepResults },
381
+ waitpointsResolved: { ...j.waitpointsResolved },
382
+ compensationsRun: { ...j.compensationsRun },
383
+ metadataState: { ...j.metadataState },
384
+ streamsCompleted: { ...j.streamsCompleted },
385
+ };
386
+ }
387
+ function createStepRunner(opts, events, now) {
388
+ return async (args) => {
389
+ const startedAt = now();
390
+ const mock = opts.steps?.[args.stepId];
391
+ try {
392
+ const output = mock ? await mock(args.stepCtx) : await args.fn(args.stepCtx);
393
+ const finishedAt = now();
394
+ events.push({ type: "step.ok", at: finishedAt, data: { stepId: args.stepId, output } });
395
+ return {
396
+ attempt: args.attempt,
397
+ status: "ok",
398
+ output,
399
+ startedAt,
400
+ finishedAt,
401
+ };
402
+ }
403
+ catch (err) {
404
+ const finishedAt = now();
405
+ const e = err;
406
+ const code = err.code ?? "UNKNOWN";
407
+ events.push({
408
+ type: "step.err",
409
+ at: finishedAt,
410
+ data: { stepId: args.stepId, message: e.message, code },
411
+ });
412
+ const retryAfter = err.retryAfter;
413
+ return {
414
+ attempt: args.attempt,
415
+ status: "err",
416
+ error: {
417
+ category: "USER_ERROR",
418
+ code,
419
+ message: e.message,
420
+ name: e.name,
421
+ stack: e.stack,
422
+ data: retryAfter !== undefined ? { retryAfter } : undefined,
423
+ },
424
+ startedAt,
425
+ finishedAt,
426
+ };
427
+ }
428
+ };
429
+ }
430
+ async function resolveWaitpoint(wp, opts, now, parentEvents, cursors) {
431
+ const at = now();
432
+ if (wp.kind === "DATETIME") {
433
+ return { kind: "DATETIME", resolvedAt: at, source: "replay" };
434
+ }
435
+ if (wp.kind === "EVENT") {
436
+ const eventType = wp.meta.eventType;
437
+ const isIter = wp.meta.iter === true;
438
+ const fixture = opts.waitForEvent?.[eventType];
439
+ if (fixture === undefined)
440
+ return null;
441
+ if (isIter) {
442
+ return resolveIterableFixture(fixture, cursors.event, eventType, at, "EVENT", `evt_test_${eventType}`);
443
+ }
444
+ const payload = Array.isArray(fixture) ? fixture[0] : fixture;
445
+ return {
446
+ kind: "EVENT",
447
+ resolvedAt: at,
448
+ matchedEventId: `evt_test_${eventType}`,
449
+ payload,
450
+ source: "live",
451
+ };
452
+ }
453
+ if (wp.kind === "SIGNAL") {
454
+ const name = wp.meta.signalName;
455
+ const isIter = wp.meta.iter === true;
456
+ const fixture = opts.waitForSignal?.[name];
457
+ if (fixture === undefined)
458
+ return null;
459
+ if (isIter) {
460
+ return resolveIterableFixture(fixture, cursors.signal, name, at, "SIGNAL");
461
+ }
462
+ return { kind: "SIGNAL", resolvedAt: at, payload: fixture, source: "live" };
463
+ }
464
+ if (wp.kind === "MANUAL") {
465
+ const tokenId = wp.meta.tokenId;
466
+ const fixture = opts.waitForToken?.[tokenId];
467
+ if (fixture === undefined)
468
+ return null;
469
+ return { kind: "MANUAL", resolvedAt: at, payload: fixture, source: "live" };
470
+ }
471
+ if (wp.kind === "RUN") {
472
+ const childWorkflowId = wp.meta.childWorkflowId;
473
+ const childInput = wp.meta.childInput;
474
+ const detach = wp.meta.detach === true;
475
+ // Allow test-level override: `invoke: { [childId]: value | (input) => value }`.
476
+ const override = opts.invoke?.[childWorkflowId];
477
+ if (override !== undefined) {
478
+ const payload = typeof override === "function"
479
+ ? await override(childInput)
480
+ : override;
481
+ parentEvents.push({
482
+ type: "child.resolved-from-fixture",
483
+ at: now(),
484
+ data: { childWorkflowId, payload, detach },
485
+ });
486
+ return {
487
+ kind: "RUN",
488
+ resolvedAt: now(),
489
+ payload: detach ? undefined : payload,
490
+ source: "replay",
491
+ };
492
+ }
493
+ // Otherwise run the child workflow in-process.
494
+ const child = getWorkflow(childWorkflowId);
495
+ if (!child) {
496
+ throw new Error(`test harness: ctx.invoke target "${childWorkflowId}" is not registered. ` +
497
+ `Import the child workflow module, or provide a stub via TestOptions.invoke.`);
498
+ }
499
+ parentEvents.push({
500
+ type: "child.started",
501
+ at: now(),
502
+ data: { childWorkflowId, input: childInput },
503
+ });
504
+ const childResult = await runWorkflowForTest(child, childInput, {
505
+ steps: opts.steps,
506
+ waitForEvent: opts.waitForEvent,
507
+ waitForSignal: opts.waitForSignal,
508
+ waitForToken: opts.waitForToken,
509
+ invoke: opts.invoke,
510
+ env: opts.env,
511
+ environment: opts.environment,
512
+ now: opts.now,
513
+ random: opts.random,
514
+ maxInvocations: opts.maxInvocations,
515
+ pauseOnWait: detach || opts.pauseOnWait,
516
+ });
517
+ parentEvents.push({
518
+ type: "child.finished",
519
+ at: now(),
520
+ data: { childWorkflowId, status: childResult.status, output: childResult.output, detach },
521
+ });
522
+ if (detach) {
523
+ return { kind: "RUN", resolvedAt: now(), payload: undefined, source: "replay" };
524
+ }
525
+ if (childResult.status === "completed") {
526
+ return { kind: "RUN", resolvedAt: now(), payload: childResult.output, source: "replay" };
527
+ }
528
+ // Child failed / cancelled / compensated with error / compensation_failed.
529
+ const err = childResult.error ?? {
530
+ category: "USER_ERROR",
531
+ code: "CHILD_RUN_ENDED",
532
+ message: `child run ended with status ${childResult.status}`,
533
+ };
534
+ return {
535
+ kind: "RUN",
536
+ resolvedAt: now(),
537
+ source: "replay",
538
+ error: err,
539
+ };
540
+ }
541
+ return null;
542
+ }
543
+ const STREAM_END_MARKER = { __voyantStreamEnd: true };
544
+ function resolveIterableFixture(fixture, cursors, key, at, kind, matchedEventId) {
545
+ const array = Array.isArray(fixture) ? fixture : [fixture];
546
+ const idx = cursors.get(key) ?? 0;
547
+ cursors.set(key, idx + 1);
548
+ if (idx >= array.length) {
549
+ // Stream is exhausted — signal the tenant-side iterator to terminate.
550
+ return {
551
+ kind,
552
+ resolvedAt: at,
553
+ payload: STREAM_END_MARKER,
554
+ source: "replay",
555
+ matchedEventId,
556
+ };
557
+ }
558
+ return {
559
+ kind,
560
+ resolvedAt: at,
561
+ payload: array[idx],
562
+ source: "live",
563
+ matchedEventId,
564
+ };
565
+ }
566
+ function applyMetadata(state, updates) {
567
+ for (const u of updates) {
568
+ switch (u.op) {
569
+ case "set":
570
+ state[u.key] = u.value;
571
+ break;
572
+ case "increment": {
573
+ const cur = typeof state[u.key] === "number" ? state[u.key] : 0;
574
+ state[u.key] = cur + (u.value ?? 1);
575
+ break;
576
+ }
577
+ case "append": {
578
+ const cur = Array.isArray(state[u.key]) ? state[u.key] : [];
579
+ state[u.key] = [...cur, u.value];
580
+ break;
581
+ }
582
+ case "remove":
583
+ delete state[u.key];
584
+ break;
585
+ }
586
+ }
587
+ }
588
+ function stepsFromJournal(j) {
589
+ return Object.entries(j.stepResults).map(([id, entry]) => ({
590
+ id,
591
+ status: entry.status,
592
+ duration: entry.finishedAt - entry.startedAt,
593
+ output: entry.output,
594
+ }));
595
+ }