@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319

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 (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +26 -1
  4. package/src/LocalPlatformRegistry.res.mjs +25 -5
  5. package/src/Platform.res +27 -2
  6. package/src/Platform.res.mjs +24 -4
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +149 -0
  34. package/src/adapter/LocalTapLines.res.mjs +232 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  38. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  39. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  40. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  41. package/src/reexport/CommandTopic.res.mjs +5 -2
  42. package/tests/LocalPlatformRegistryTest.res +17 -0
  43. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  44. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  45. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  46. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  47. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  48. package/tests/adapter/LocalFeedTest.res +314 -0
  49. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  50. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  51. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  52. package/tests/adapter/LocalTapLinesTest.res +207 -0
  53. package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
  54. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  55. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  56. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  57. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  58. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  59. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  60. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  61. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  62. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  63. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  64. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
@@ -3,6 +3,8 @@
3
3
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
7
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
6
8
  import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
7
9
  import * as AutomationSliceFixtures$ReventlessLocal from "./AutomationSliceFixtures.res.mjs";
8
10
  import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
@@ -280,6 +282,85 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
280
282
  globalThis.expect(publishedCommands2.contents.length).toBe(0);
281
283
  });
282
284
  });
285
+ globalThis.describe("the flow", () => {
286
+ let withMeta = (json, msgId, correlationId) => {
287
+ if (typeof json !== "object" || json === null || Array.isArray(json)) {
288
+ return json;
289
+ }
290
+ let meta_service = AutomationSliceFixtures$ReventlessLocal.ShipOrderSource.name;
291
+ let meta_user = "user-7";
292
+ let meta_traceparent = "00-trace-span-01";
293
+ let meta = {
294
+ service: meta_service,
295
+ time: "2026-01-01T00:00:00Z",
296
+ user: meta_user,
297
+ msgId: msgId,
298
+ correlationId: correlationId,
299
+ traceparent: meta_traceparent
300
+ };
301
+ json["meta"] = Util_Sury$Reventless.toJson(meta, Message$Reventless.metaSchema);
302
+ return json;
303
+ };
304
+ globalThis.test("the command derives its meta from the event that created the row", async () => {
305
+ let json = withMeta(AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
306
+ TAG: "OrderPlaced",
307
+ orderId: "ord-1",
308
+ address: "123 Main St"
309
+ }), "evt-1", "flow-1");
310
+ await Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
311
+ globalThis.expect(Callback.todoItems["ord-1"].source).toEqual({
312
+ msgId: "evt-1",
313
+ correlationId: "flow-1",
314
+ user: "user-7",
315
+ traceparent: "00-trace-span-01"
316
+ });
317
+ let published = {
318
+ contents: []
319
+ };
320
+ await Callback.phase2(async cmds => {
321
+ published.contents = cmds;
322
+ });
323
+ let meta = published.contents[0].meta;
324
+ globalThis.expect(meta.correlationId).toBe("flow-1");
325
+ globalThis.expect(meta.causationId).toEqual("evt-1");
326
+ globalThis.expect(meta.user).toEqual("user-7");
327
+ globalThis.expect(meta.traceparent).toEqual("00-trace-span-01");
328
+ globalThis.expect(meta.service).toBe(AutomationSliceFixtures$ReventlessLocal.ShipOrderSpec.targetName);
329
+ globalThis.expect(meta.msgId !== "evt-1").toBe(true);
330
+ });
331
+ globalThis.test("a stored row without a source decodes and starts a new flow", async () => {
332
+ let stored = JSON.parse(`{"item":{"orderId":"ord-9","address":"x"},"status":"Pending","createdAt":"2026-01-01T00:00:00Z","retryCount":0}`);
333
+ let row = Util_Sury$Reventless.fromJson(stored, AutomationSlice_Callback$ReventlessCore.todoRowSchema);
334
+ globalThis.expect(row.source).toEqual(undefined);
335
+ Callback.todoItems["ord-9"] = row;
336
+ let published = {
337
+ contents: []
338
+ };
339
+ await Callback.phase2(async cmds => {
340
+ published.contents = cmds;
341
+ });
342
+ let meta = published.contents[0].meta;
343
+ globalThis.expect(meta.causationId).toEqual(undefined);
344
+ globalThis.expect(meta.correlationId).toBe(meta.msgId);
345
+ });
346
+ globalThis.test("a row with a source round-trips through its schema", () => {
347
+ let row_item = {};
348
+ let row_source = {
349
+ msgId: "evt-1",
350
+ correlationId: "flow-1"
351
+ };
352
+ let row = {
353
+ item: row_item,
354
+ status: "Pending",
355
+ createdAt: "2026-01-01T00:00:00Z",
356
+ retryCount: 0,
357
+ source: row_source
358
+ };
359
+ let json = Util_Sury$Reventless.toJson(row, AutomationSlice_Callback$ReventlessCore.todoRowSchema);
360
+ globalThis.expect(JSON.stringify(json).includes(`"source":{"msgId":"evt-1"`)).toBe(true);
361
+ globalThis.expect(Util_Sury$Reventless.fromJson(json, AutomationSlice_Callback$ReventlessCore.todoRowSchema)).toEqual(row);
362
+ });
363
+ });
283
364
  });
284
365
 
285
366
  export {
@@ -191,7 +191,11 @@ let dcbEventLog = DcbLogMaker.make(
191
191
  // CommandTopic without the runtime wiring overhead.)
192
192
  // ─────────────────────────────────────────────────────────────
193
193
 
194
+ // Every command sent, in order, so a test can read the metas they carried.
195
+ let sentCommands: ref<array<Reventless.Message.commandJson>> = ref([])
196
+
194
197
  let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmdJsons => {
198
+ sentCommands := sentCommands.contents->Array.concat(cmdJsons)
195
199
  let _ = await cmdJsons
196
200
  ->Array.map(async cmdJson => {
197
201
  let typeName = switch cmdJson.commandJson {
@@ -292,3 +296,16 @@ let readEventTypes = async (orderId: string) => {
292
296
  )
293
297
  result.events->Array.map(e => e.eventType)
294
298
  }
299
+
300
+ let readEvents = async (orderId: string) => {
301
+ let logOps = await dcbEventLog->DcbLogMaker.operations->TestRunner.resolve
302
+ let result = await logOps.read(
303
+ ~query=[{tags: [{Reventless.DcbTag.key: "orderId", value: orderId}]}],
304
+ )
305
+ result.events
306
+ }
307
+
308
+ let pause = (ms: int): promise<unit> =>
309
+ Promise.make((resolve, _) => {
310
+ let _ = setTimeout(() => resolve(), ms)
311
+ })
@@ -323,7 +323,12 @@ let dcbEventLog = DcbLogMaker.make("TestLog", undefined, {
323
323
  }
324
324
  }, undefined);
325
325
 
326
+ let sentCommands = {
327
+ contents: []
328
+ };
329
+
326
330
  async function publishJsons(cmdJsons) {
331
+ sentCommands.contents = sentCommands.contents.concat(cmdJsons);
327
332
  await Promise.all(cmdJsons.map(async cmdJson => {
328
333
  let dict = cmdJson.commandJson;
329
334
  let typeName;
@@ -459,6 +464,22 @@ async function readEventTypes(orderId) {
459
464
  return result.events.map(e => e.eventType);
460
465
  }
461
466
 
467
+ async function readEvents(orderId) {
468
+ let logOps = await TestRunner$ReventlessLocal.resolve(DcbLogMaker.operations(dcbEventLog));
469
+ return (await logOps.read([{
470
+ tags: [{
471
+ key: "orderId",
472
+ value: orderId
473
+ }]
474
+ }], undefined)).events;
475
+ }
476
+
477
+ function pause(ms) {
478
+ return new Promise((resolve, param) => {
479
+ setTimeout(() => resolve(), ms);
480
+ });
481
+ }
482
+
462
483
  export {
463
484
  PlaceSpec,
464
485
  PlaceDecision,
@@ -471,6 +492,7 @@ export {
471
492
  Bus,
472
493
  DcbLogMaker,
473
494
  dcbEventLog,
495
+ sentCommands,
474
496
  publishJsons,
475
497
  publishJsonsOutput,
476
498
  PlaceMaker,
@@ -486,5 +508,7 @@ export {
486
508
  placeCmdJson,
487
509
  withTimeout,
488
510
  readEventTypes,
511
+ readEvents,
512
+ pause,
489
513
  }
490
514
  /* moduleUrl Not a pure module */
@@ -44,4 +44,34 @@ describe("AutomationSlice self-deadlock regression:", () => {
44
44
  expect(eventTypes->Array.includes("Placed"))->toBe(true)
45
45
  expect(eventTypes->Array.includes("Shipped"))->toBe(true)
46
46
  })
47
+
48
+ // The automation's command continues the flow of the event that created its
49
+ // row: one correlation id from the first command to the last event, and each
50
+ // message caused by the one before it.
51
+ testPromise("the automation's command and its events stay in the event's flow", async () => {
52
+ sentCommands := []
53
+ let _ = await withTimeout(publishJsons([placeCmdJson("order-2")]), ~ms=1500)
54
+ let shipped = async () =>
55
+ (await readEvents("order-2"))->Array.find(e => e.eventType == "Shipped")
56
+ let waited = ref(0)
57
+ while (await shipped())->Option.isNone && waited.contents < 100 {
58
+ await pause(10)
59
+ waited := waited.contents + 1
60
+ }
61
+
62
+ let events = await readEvents("order-2")
63
+ let placed = events->Array.find(e => e.eventType == "Placed")->Option.getOrThrow
64
+ let shipped = events->Array.find(e => e.eventType == "Shipped")->Option.getOrThrow
65
+ let ship =
66
+ sentCommands.contents
67
+ ->Array.find(c => c.id == "order-2" && c.meta.msgId != TestFixtures.testMeta.msgId)
68
+ ->Option.getOrThrow
69
+
70
+ expect(placed.meta.correlationId)->toBe(TestFixtures.testMeta.correlationId)
71
+ expect(ship.meta.correlationId)->toBe(TestFixtures.testMeta.correlationId)
72
+ expect(shipped.meta.correlationId)->toBe(TestFixtures.testMeta.correlationId)
73
+ expect(ship.meta.causationId)->toEqual(Some(placed.meta.msgId))
74
+ expect(shipped.meta.causationId)->toEqual(Some(ship.meta.msgId))
75
+ expect(ship.meta.user)->toEqual(TestFixtures.testMeta.user)
76
+ })
47
77
  })
@@ -1,7 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
4
5
  import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
6
+ import * as TestFixtures$ReventlessLocal from "../../TestFixtures.res.mjs";
5
7
  import * as AutomationSliceSelfDeadlockFixtures$ReventlessLocal from "./AutomationSliceSelfDeadlockFixtures.res.mjs";
6
8
 
7
9
  globalThis.describe("AutomationSlice self-deadlock regression:", () => {
@@ -26,6 +28,32 @@ globalThis.describe("AutomationSlice self-deadlock regression:", () => {
26
28
  globalThis.expect(eventTypes.includes("Placed")).toBe(true);
27
29
  globalThis.expect(eventTypes.includes("Shipped")).toBe(true);
28
30
  });
31
+ globalThis.test("the automation's command and its events stay in the event's flow", async () => {
32
+ AutomationSliceSelfDeadlockFixtures$ReventlessLocal.sentCommands.contents = [];
33
+ await AutomationSliceSelfDeadlockFixtures$ReventlessLocal.withTimeout(AutomationSliceSelfDeadlockFixtures$ReventlessLocal.publishJsons([AutomationSliceSelfDeadlockFixtures$ReventlessLocal.placeCmdJson("order-2")]), 1500);
34
+ let shipped = async () => (await AutomationSliceSelfDeadlockFixtures$ReventlessLocal.readEvents("order-2")).find(e => e.eventType === "Shipped");
35
+ let waited = 0;
36
+ while (Stdlib_Option.isNone(await shipped()) && waited < 100) {
37
+ await AutomationSliceSelfDeadlockFixtures$ReventlessLocal.pause(10);
38
+ waited = waited + 1 | 0;
39
+ };
40
+ let events = await AutomationSliceSelfDeadlockFixtures$ReventlessLocal.readEvents("order-2");
41
+ let placed = Stdlib_Option.getOrThrow(events.find(e => e.eventType === "Placed"), undefined);
42
+ let shipped$1 = Stdlib_Option.getOrThrow(events.find(e => e.eventType === "Shipped"), undefined);
43
+ let ship = Stdlib_Option.getOrThrow(AutomationSliceSelfDeadlockFixtures$ReventlessLocal.sentCommands.contents.find(c => {
44
+ if (c.id === "order-2") {
45
+ return c.meta.msgId !== TestFixtures$ReventlessLocal.testMeta.msgId;
46
+ } else {
47
+ return false;
48
+ }
49
+ }), undefined);
50
+ globalThis.expect(placed.meta.correlationId).toBe(TestFixtures$ReventlessLocal.testMeta.correlationId);
51
+ globalThis.expect(ship.meta.correlationId).toBe(TestFixtures$ReventlessLocal.testMeta.correlationId);
52
+ globalThis.expect(shipped$1.meta.correlationId).toBe(TestFixtures$ReventlessLocal.testMeta.correlationId);
53
+ globalThis.expect(ship.meta.causationId).toEqual(placed.meta.msgId);
54
+ globalThis.expect(shipped$1.meta.causationId).toEqual(ship.meta.msgId);
55
+ globalThis.expect(ship.meta.user).toEqual(TestFixtures$ReventlessLocal.testMeta.user);
56
+ });
29
57
  });
30
58
 
31
59
  /* Not a pure module */
@@ -472,4 +472,62 @@ describe("OutboundTranslationSlice Callback", () => {
472
472
  },
473
473
  )
474
474
  })
475
+
476
+ // A row keeps the event that created it, so the command sent back continues
477
+ // the event's flow; without a meta, or for a row stored before, it starts one.
478
+ describe("the flow", () => {
479
+ let eventMeta: Reventless.Message.meta = {
480
+ service: "Payments",
481
+ time: "2026-01-01T00:00:00Z",
482
+ user: "user-7",
483
+ msgId: "evt-1",
484
+ correlationId: "flow-1",
485
+ }
486
+ let publishInto = (published: ref<array<Reventless.Message.commandJson>>) =>
487
+ async cmds => published := published.contents->Array.concat(cmds)
488
+
489
+ testPromise(
490
+ "the command sent back derives its meta from the event",
491
+ async () => {
492
+ await CommandBackCallback.phase1(
493
+ [("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))],
494
+ ~metas=[Some(eventMeta)],
495
+ ~capabilities=Reventless.Capabilities.none,
496
+ )
497
+ let published = ref([])
498
+ await CommandBackCallback.phase2(
499
+ publishInto(published),
500
+ ~capabilities=Reventless.Capabilities.none,
501
+ )
502
+ let meta = (published.contents->Array.getUnsafe(0)).meta
503
+ expect(meta.correlationId)->toBe("flow-1")
504
+ expect(meta.causationId)->toEqual(Some("evt-1"))
505
+ expect(meta.user)->toEqual(Some("user-7"))
506
+ expect(meta.service)->toBe("ConfirmPayment")
507
+ },
508
+ )
509
+
510
+ testPromise(
511
+ "without a meta, or from a stored row without a source, it starts a new flow",
512
+ async () => {
513
+ await CommandBackCallback.phase1(
514
+ [("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))],
515
+ ~capabilities=Reventless.Capabilities.none,
516
+ )
517
+ let stored =
518
+ JSON.parseOrThrow(`{"item":{"orderId":"ord-2","amount":5},"status":"Pending","createdAt":"2026-01-01T00:00:00Z","retryCount":0}`)->Reventless.Util_Sury.fromJson(
519
+ todoRowSchema,
520
+ )
521
+ expect(stored.source)->toEqual(None)
522
+ CommandBackCallback.todoItems->Dict.set("ord-2", stored)
523
+ let published = ref([])
524
+ await CommandBackCallback.phase2(
525
+ publishInto(published),
526
+ ~capabilities=Reventless.Capabilities.none,
527
+ )
528
+ expect(published.contents->Array.length)->toBe(2)
529
+ published.contents->Array.forEach(c => expect(c.meta.causationId)->toEqual(None))
530
+ },
531
+ )
532
+ })
475
533
  })
@@ -3,6 +3,7 @@
3
3
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
6
7
  import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
7
8
  import * as OutboundTranslationSliceFixtures$ReventlessLocal from "./OutboundTranslationSliceFixtures.res.mjs";
8
9
  import * as OutboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
@@ -90,7 +91,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
90
91
  orderId: "ord-1",
91
92
  email: "test@example.com"
92
93
  }
93
- ]], Capabilities$Reventless.none);
94
+ ]], undefined, Capabilities$Reventless.none);
94
95
  let items = FireForgetCallback.todoItems;
95
96
  globalThis.expect(Object.entries(items).length).toBe(1);
96
97
  let row = items["ord-1"];
@@ -107,7 +108,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
107
108
  orderId: "ord-1",
108
109
  email: "test@example.com"
109
110
  }
110
- ]], Capabilities$Reventless.none);
111
+ ]], undefined, Capabilities$Reventless.none);
111
112
  await FireForgetCallback.phase1([[
112
113
  "evt",
113
114
  {
@@ -115,7 +116,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
115
116
  orderId: "ord-1",
116
117
  email: "other@example.com"
117
118
  }
118
- ]], Capabilities$Reventless.none);
119
+ ]], undefined, Capabilities$Reventless.none);
119
120
  globalThis.expect(Object.entries(FireForgetCallback.todoItems).length).toBe(1);
120
121
  });
121
122
  globalThis.test("multiple different events create multiple items", async () => {
@@ -136,7 +137,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
136
137
  email: "b@example.com"
137
138
  }
138
139
  ]
139
- ], Capabilities$Reventless.none);
140
+ ], undefined, Capabilities$Reventless.none);
140
141
  globalThis.expect(Object.entries(FireForgetCallback.todoItems).length).toBe(2);
141
142
  });
142
143
  });
@@ -149,7 +150,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
149
150
  orderId: "ord-1",
150
151
  email: "test@example.com"
151
152
  }
152
- ]], Capabilities$Reventless.none);
153
+ ]], undefined, Capabilities$Reventless.none);
153
154
  let publishedCommands = {
154
155
  contents: []
155
156
  };
@@ -181,7 +182,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
181
182
  orderId: "ord-1",
182
183
  amount: 50.0
183
184
  }
184
- ]], Capabilities$Reventless.none);
185
+ ]], undefined, Capabilities$Reventless.none);
185
186
  let publishedCommands = {
186
187
  contents: []
187
188
  };
@@ -203,7 +204,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
203
204
  orderId: "ord-1",
204
205
  amount: 50.0
205
206
  }
206
- ]], Capabilities$Reventless.none);
207
+ ]], undefined, Capabilities$Reventless.none);
207
208
  let publishedCommands = {
208
209
  contents: []
209
210
  };
@@ -226,7 +227,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
226
227
  orderId: "ord-1",
227
228
  amount: 50.0
228
229
  }
229
- ]], Capabilities$Reventless.none);
230
+ ]], undefined, Capabilities$Reventless.none);
230
231
  let mockPublish = async _cmds => {};
231
232
  await CommandBackCallback.phase2(mockPublish, Capabilities$Reventless.none);
232
233
  let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
@@ -245,7 +246,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
245
246
  orderId: "ord-1",
246
247
  amount: 50.0
247
248
  }
248
- ]], Capabilities$Reventless.none);
249
+ ]], undefined, Capabilities$Reventless.none);
249
250
  let mockPublish = async _cmds => {};
250
251
  await CommandBackCallback.phase2(mockPublish, Capabilities$Reventless.none);
251
252
  let row1 = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
@@ -283,7 +284,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
283
284
  orderId: "ord-1",
284
285
  amount: 50.0
285
286
  }
286
- ]], Capabilities$Reventless.none);
287
+ ]], undefined, Capabilities$Reventless.none);
287
288
  let mockPublish = async _cmds => {};
288
289
  await CommandBackCallback.phase2(mockPublish, Capabilities$Reventless.none);
289
290
  globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).status).toBe("Failed");
@@ -315,7 +316,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
315
316
  orderId: "ord-1",
316
317
  amount: 50.0
317
318
  }
318
- ]], Capabilities$Reventless.none);
319
+ ]], undefined, Capabilities$Reventless.none);
319
320
  let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
320
321
  let newrecord = {...row};
321
322
  newrecord.retryCount = 2;
@@ -336,7 +337,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
336
337
  orderId: "ord-1",
337
338
  amount: 50.0
338
339
  }
339
- ]], Capabilities$Reventless.none);
340
+ ]], undefined, Capabilities$Reventless.none);
340
341
  let publish = async _cmds => {};
341
342
  await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
342
343
  await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
@@ -352,7 +353,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
352
353
  orderId: "ord-1",
353
354
  amount: 50.0
354
355
  }
355
- ]], Capabilities$Reventless.none);
356
+ ]], undefined, Capabilities$Reventless.none);
356
357
  globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).maxRetries).toEqual(OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.maxRetries);
357
358
  });
358
359
  globalThis.test("a slice that answers onExhausted has its command published", async () => {
@@ -374,7 +375,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
374
375
  orderId: "ord-1",
375
376
  amount: 50.0
376
377
  }
377
- ]], Capabilities$Reventless.none);
378
+ ]], undefined, Capabilities$Reventless.none);
378
379
  let published = {
379
380
  contents: []
380
381
  };
@@ -407,7 +408,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
407
408
  orderId: "ord-1",
408
409
  amount: 50.0
409
410
  }
410
- ]], Capabilities$Reventless.none);
411
+ ]], undefined, Capabilities$Reventless.none);
411
412
  let publish = async _cmds => {};
412
413
  await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
413
414
  await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
@@ -425,7 +426,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
425
426
  orderId: "ord-1",
426
427
  amount: 50.0
427
428
  }
428
- ]], Capabilities$Reventless.none);
429
+ ]], undefined, Capabilities$Reventless.none);
429
430
  let published = {
430
431
  contents: []
431
432
  };
@@ -456,7 +457,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
456
457
  orderId: "ord-1",
457
458
  amount: 50.0
458
459
  }
459
- ]], Capabilities$Reventless.none);
460
+ ]], undefined, Capabilities$Reventless.none);
460
461
  let failOnAbandonment = async _cmds => Stdlib_JsError.throwWithMessage("topic unavailable");
461
462
  await CommandBackCallback.phase2(failOnAbandonment, Capabilities$Reventless.none);
462
463
  await CommandBackCallback.phase2(failOnAbandonment, Capabilities$Reventless.none);
@@ -479,7 +480,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
479
480
  orderId: "ord-1",
480
481
  amount: 50.0
481
482
  }
482
- ]], Capabilities$Reventless.none);
483
+ ]], undefined, Capabilities$Reventless.none);
483
484
  let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
484
485
  let newrecord = {...row};
485
486
  newrecord.retryCount = 2;
@@ -535,7 +536,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
535
536
  amount: 75.0
536
537
  }
537
538
  ]
538
- ], Capabilities$Reventless.none);
539
+ ], undefined, Capabilities$Reventless.none);
539
540
  let publishedCommands = {
540
541
  contents: []
541
542
  };
@@ -550,6 +551,57 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
550
551
  globalThis.expect(publishedCommands.contents.length).toBe(1);
551
552
  });
552
553
  });
554
+ globalThis.describe("the flow", () => {
555
+ let publishInto = published => (async cmds => {
556
+ published.contents = published.contents.concat(cmds);
557
+ });
558
+ globalThis.test("the command sent back derives its meta from the event", async () => {
559
+ await CommandBackCallback.phase1([[
560
+ "evt",
561
+ {
562
+ TAG: "PaymentReceived",
563
+ orderId: "ord-1",
564
+ amount: 50.0
565
+ }
566
+ ]], [{
567
+ service: "Payments",
568
+ time: "2026-01-01T00:00:00Z",
569
+ user: "user-7",
570
+ msgId: "evt-1",
571
+ correlationId: "flow-1"
572
+ }], Capabilities$Reventless.none);
573
+ let published = {
574
+ contents: []
575
+ };
576
+ await CommandBackCallback.phase2(publishInto(published), Capabilities$Reventless.none);
577
+ let meta = published.contents[0].meta;
578
+ globalThis.expect(meta.correlationId).toBe("flow-1");
579
+ globalThis.expect(meta.causationId).toEqual("evt-1");
580
+ globalThis.expect(meta.user).toEqual("user-7");
581
+ globalThis.expect(meta.service).toBe("ConfirmPayment");
582
+ });
583
+ globalThis.test("without a meta, or from a stored row without a source, it starts a new flow", async () => {
584
+ await CommandBackCallback.phase1([[
585
+ "evt",
586
+ {
587
+ TAG: "PaymentReceived",
588
+ orderId: "ord-1",
589
+ amount: 50.0
590
+ }
591
+ ]], undefined, Capabilities$Reventless.none);
592
+ let stored = Util_Sury$Reventless.fromJson(JSON.parse(`{"item":{"orderId":"ord-2","amount":5},"status":"Pending","createdAt":"2026-01-01T00:00:00Z","retryCount":0}`), OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema);
593
+ globalThis.expect(stored.source).toEqual(undefined);
594
+ CommandBackCallback.todoItems["ord-2"] = stored;
595
+ let published = {
596
+ contents: []
597
+ };
598
+ await CommandBackCallback.phase2(publishInto(published), Capabilities$Reventless.none);
599
+ globalThis.expect(published.contents.length).toBe(2);
600
+ published.contents.forEach(c => {
601
+ globalThis.expect(c.meta.causationId).toEqual(undefined);
602
+ });
603
+ });
604
+ });
553
605
  });
554
606
 
555
607
  export {
@@ -225,7 +225,7 @@ globalThis.describe("an outbound work item with personal data", () => {
225
225
  await Callback.phase1([[
226
226
  "c-1",
227
227
  given
228
- ]], keyed);
228
+ ]], undefined, keyed);
229
229
  let stored = Stdlib_Option.getOr(JSON.stringify(Object.entries(Callback.todoItems)), "");
230
230
  globalThis.expect(stored.includes("ada@example.org")).toBe(false);
231
231
  let match = Object.entries(Callback.todoItems);
@@ -241,18 +241,18 @@ globalThis.describe("an outbound work item with personal data", () => {
241
241
  await Callback.phase1([[
242
242
  "c-1",
243
243
  given
244
- ]], keyed);
244
+ ]], undefined, keyed);
245
245
  await Callback.phase1([[
246
246
  "c-1",
247
247
  given
248
- ]], keyed);
248
+ ]], undefined, keyed);
249
249
  globalThis.expect(Object.entries(Callback.todoItems).length).toBe(1);
250
250
  });
251
251
  globalThis.test("is opened before it is translated", async () => {
252
252
  await Callback.phase1([[
253
253
  "c-1",
254
254
  given
255
- ]], keyed);
255
+ ]], undefined, keyed);
256
256
  await Callback.phase2(publish, keyed);
257
257
  globalThis.expect(sent.contents).toEqual(["ada@example.org"]);
258
258
  });
@@ -260,7 +260,7 @@ globalThis.describe("an outbound work item with personal data", () => {
260
260
  await Callback.phase1([[
261
261
  "c-1",
262
262
  given
263
- ]], keyed);
263
+ ]], undefined, keyed);
264
264
  erased.contents = true;
265
265
  await Callback.phase2(publish, keyed);
266
266
  globalThis.expect(sent.contents).toEqual([]);
@@ -270,7 +270,7 @@ globalThis.describe("an outbound work item with personal data", () => {
270
270
  await Callback.phase1([[
271
271
  "c-1",
272
272
  given
273
- ]], Capabilities$Reventless.none);
273
+ ]], undefined, Capabilities$Reventless.none);
274
274
  globalThis.expect(Object.entries(Callback.todoItems).map(prim => prim[0])).toEqual(["c-1:ada@example.org"]);
275
275
  });
276
276
  globalThis.test("is not stored at all where encryption is required and nothing can fingerprint it", async () => {
@@ -278,7 +278,7 @@ globalThis.describe("an outbound work item with personal data", () => {
278
278
  await Callback.phase1([[
279
279
  "c-1",
280
280
  given
281
- ]], Capabilities$Reventless.none);
281
+ ]], undefined, Capabilities$Reventless.none);
282
282
  globalThis.expect(Object.entries(Callback.todoItems)).toEqual([]);
283
283
  });
284
284
  });