@reventlessdev/reventless-local 3.0.0-alpha.113 → 3.0.0-alpha.115

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 (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +25 -50
  4. package/src/Platform.res.mjs +33 -160
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +10 -1
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +9 -1
  7. package/src/adapter/EventLog/EventLogStorage_InMemory.res +1 -1
  8. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +1 -1
  9. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +16 -3
  10. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +35 -7
  11. package/src/adapter/LocalBus.res +23 -3
  12. package/src/adapter/LocalBus.res.mjs +100 -66
  13. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +99 -7
  14. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +97 -13
  15. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +24 -1
  16. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +19 -4
  17. package/src/adapter/SqliteDriver.res +6 -1
  18. package/src/adapter/SqliteDriver.res.mjs +5 -1
  19. package/src/components/DcbEventLog_Builder.res +7 -1
  20. package/src/components/DcbEventLog_Builder.res.mjs +3 -3
  21. package/src/test/Mocks/MockEventLogStorage.res +1 -1
  22. package/src/test/Mocks/MockEventLogStorage.res.mjs +4 -1
  23. package/tests/adapter/BackendParityTest.res +64 -0
  24. package/tests/adapter/BackendParityTest.res.mjs +45 -0
  25. package/tests/adapter/EventLogStorageSqliteTest.res +4 -2
  26. package/tests/adapter/EventLogStorageSqliteTest.res.mjs +6 -1
  27. package/tests/adapter/LocalBusPubSubTest.res +31 -0
  28. package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -0
  29. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +2 -0
  30. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +3 -0
  31. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +1 -0
  32. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +1 -0
  33. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +4 -2
  34. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +2 -0
  35. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +4 -2
@@ -265,7 +265,19 @@ module Impl = (C: BusConfig): T => {
265
265
  let drainLoop = Effect.scoped(
266
266
  PubSub.subscribe(hub)->Effect.flatMap(queue =>
267
267
  Stream.fromQueue(queue)->Stream.runForEach(msg =>
268
- Effect.promise(() => handler(msg.service, msg.meta, msg.json))->Effect.zipRight(msg.done_)
268
+ // A throwing/rejecting handler must not (a) leave `done_` uncalled —
269
+ // the publisher's countdown would never reach zero and every publish
270
+ // on this topic would hang — nor (b) kill this drain fiber, which
271
+ // would stop consuming while the subscriber stays counted, hanging all
272
+ // future publishes too. So: convert a rejection into a typed error
273
+ // (`tryPromise`), log-and-recover (`catchAll`) so the stream keeps
274
+ // draining, and run `done_` via `ensuring` so the countdown always
275
+ // advances regardless of outcome.
276
+ Effect.tryPromise(~catch=e => e, () => handler(msg.service, msg.meta, msg.json))
277
+ ->Effect.catchAll(err =>
278
+ Effect.sync(() => Console.error2("[LocalBus] subscriber handler failed:", err))
279
+ )
280
+ ->Effect.ensuring(msg.done_)
269
281
  )
270
282
  ),
271
283
  )
@@ -372,7 +384,11 @@ module Impl = (C: BusConfig): T => {
372
384
  let pending = queue.contents
373
385
  queue.contents = []
374
386
  pending->Array.forEach(json => {
375
- let _ = handler(json, ())
387
+ let _ =
388
+ handler(json, ())->Promise.catch(e => {
389
+ Console.error2("[LocalBus] parked command handler failed:", e)
390
+ Promise.resolve()
391
+ })
376
392
  })
377
393
  | None => ()
378
394
  }
@@ -414,7 +430,11 @@ module Impl = (C: BusConfig): T => {
414
430
  // and done_ is called immediately. This prevents a deadlock where publishEvent on the
415
431
  // EP topic would block until the downstream aggregate command chain completes.
416
432
  let makeFireAndForgetHandler = handler => (_, _, json) => {
417
- let _ = handler(json, ())
433
+ let _ =
434
+ handler(json, ())->Promise.catch(e => {
435
+ Console.error2("[LocalBus] fire-and-forget handler failed:", e)
436
+ Promise.resolve()
437
+ })
418
438
  Promise.resolve()
419
439
  }
420
440
 
@@ -1,12 +1,14 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Effect from "@reventlessdev/rescript-effect/src/Effect.res.mjs";
3
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
5
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
6
  import * as Queue from "effect/Queue";
6
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
- import * as Effect from "effect/Effect";
8
+ import * as Effect$1 from "effect/Effect";
8
9
  import * as PubSub from "effect/PubSub";
9
10
  import * as Stream from "effect/Stream";
11
+ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
10
12
  import * as Deferred from "effect/Deferred";
11
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
12
14
 
@@ -110,9 +112,9 @@ function Impl(C) {
110
112
  };
111
113
  let makeHub = () => {
112
114
  if (capacity !== undefined) {
113
- return Effect.runSync(PubSub.bounded(capacity));
115
+ return Effect$1.runSync(PubSub.bounded(capacity));
114
116
  } else {
115
- return Effect.runSync(PubSub.unbounded());
117
+ return Effect$1.runSync(PubSub.unbounded());
116
118
  }
117
119
  };
118
120
  let subscribeToEvents = (topicName, handler) => {
@@ -127,8 +129,10 @@ function Impl(C) {
127
129
  }
128
130
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
129
131
  subscriberCounts.contents[topicName] = n + 1 | 0;
130
- let drainLoop = Effect.scoped(Effect.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect.zipRight(Effect.promise(() => handler(msg.service, msg.meta, msg.json)), msg.done_))));
131
- Effect.runFork(drainLoop);
132
+ let drainLoop = Effect$1.scoped(Effect$1.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect$1.ensuring(Effect$1.catchAll(Effect.tryPromise(e => e, () => handler(msg.service, msg.meta, msg.json)), err => Effect$1.sync(() => {
133
+ console.error("[LocalBus] subscriber handler failed:", err);
134
+ })), msg.done_))));
135
+ Effect$1.runFork(drainLoop);
132
136
  };
133
137
  let subscribeToEventStream = topicName => {
134
138
  let h = eventHubs.contents[topicName];
@@ -140,10 +144,10 @@ function Impl(C) {
140
144
  eventHubs.contents[topicName] = h$1;
141
145
  hub = h$1;
142
146
  }
143
- return Effect.map(Effect.acquireRelease(Effect.flatMap(Effect.sync(() => {
147
+ return Effect$1.map(Effect$1.acquireRelease(Effect$1.flatMap(Effect$1.sync(() => {
144
148
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
145
149
  subscriberCounts.contents[topicName] = n + 1 | 0;
146
- }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect.zipRight(Effect.sync(() => {
150
+ }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect$1.zipRight(Effect$1.sync(() => {
147
151
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 1);
148
152
  subscriberCounts.contents[topicName] = n - 1 | 0;
149
153
  }), Queue.shutdown(queue))), queue => Stream.fromQueue(queue));
@@ -161,17 +165,17 @@ function Impl(C) {
161
165
  if (n === 0) {
162
166
  return;
163
167
  }
164
- let allDone = Effect.runSync(Deferred.make());
168
+ let allDone = Effect$1.runSync(Deferred.make());
165
169
  let remaining = {
166
170
  contents: n
167
171
  };
168
- let done_ = Effect.flatMap(Effect.sync(() => {
172
+ let done_ = Effect$1.flatMap(Effect$1.sync(() => {
169
173
  remaining.contents = remaining.contents - 1 | 0;
170
174
  }), () => {
171
175
  if (remaining.contents === 0) {
172
- return Effect.map(Deferred.succeed(allDone, undefined), param => {});
176
+ return Effect$1.map(Deferred.succeed(allDone, undefined), param => {});
173
177
  } else {
174
- return Effect.succeed();
178
+ return Effect$1.succeed();
175
179
  }
176
180
  });
177
181
  let msg = {
@@ -180,10 +184,10 @@ function Impl(C) {
180
184
  json: json,
181
185
  done_: done_
182
186
  };
183
- let publishAndWait = capacity !== undefined ? Effect.flatMap(PubSub.publish(hub$1, msg), param => Deferred.await(allDone)) : Effect.zipRight(Effect.sync(() => {
184
- Effect.runSync(PubSub.publish(hub$1, msg));
187
+ let publishAndWait = capacity !== undefined ? Effect$1.flatMap(PubSub.publish(hub$1, msg), param => Deferred.await(allDone)) : Effect$1.zipRight(Effect$1.sync(() => {
188
+ Effect$1.runSync(PubSub.publish(hub$1, msg));
185
189
  }), Deferred.await(allDone));
186
- await Effect.runPromise(publishAndWait);
190
+ await Effect$1.runPromise(publishAndWait);
187
191
  };
188
192
  let dispatchCommand = async (channelName, json) => {
189
193
  let handler = commandHandlers.contents[channelName];
@@ -212,7 +216,10 @@ function Impl(C) {
212
216
  let pending = queue.contents;
213
217
  queue.contents = [];
214
218
  pending.forEach(json => {
215
- handler(json, undefined);
219
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
220
+ console.error("[LocalBus] parked command handler failed:", e);
221
+ return Promise.resolve();
222
+ });
216
223
  });
217
224
  };
218
225
  let registerQueryDb = (name, ops) => {
@@ -252,7 +259,10 @@ function Impl(C) {
252
259
  contents: {}
253
260
  };
254
261
  let makeFireAndForgetHandler = handler => ((param, param$1, json) => {
255
- handler(json, undefined);
262
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
263
+ console.error("[LocalBus] fire-and-forget handler failed:", e);
264
+ return Promise.resolve();
265
+ });
256
266
  return Promise.resolve();
257
267
  });
258
268
  let registerEventCollectorHandler = (ecName, handler) => {
@@ -273,10 +283,10 @@ function Impl(C) {
273
283
  eventCollectorPendingTopics.contents[ecName] = pending;
274
284
  };
275
285
  let reset = () => {
276
- let shutdownAll = Effect.map(Effect.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
286
+ let shutdownAll = Effect$1.map(Effect$1.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
277
287
  concurrency: "unbounded"
278
288
  }), param => {});
279
- Effect.runSync(shutdownAll);
289
+ Effect$1.runSync(shutdownAll);
280
290
  eventHubs.contents = {};
281
291
  subscriberCounts.contents = {};
282
292
  commandHandlers.contents = {};
@@ -342,7 +352,7 @@ function Make($star) {
342
352
  let dcbEventLogReadRegistry = {
343
353
  contents: {}
344
354
  };
345
- let makeHub = () => Effect.runSync(PubSub.unbounded());
355
+ let makeHub = () => Effect$1.runSync(PubSub.unbounded());
346
356
  let subscribeToEvents = (topicName, handler) => {
347
357
  let h = eventHubs.contents[topicName];
348
358
  let hub;
@@ -355,8 +365,10 @@ function Make($star) {
355
365
  }
356
366
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
357
367
  subscriberCounts.contents[topicName] = n + 1 | 0;
358
- let drainLoop = Effect.scoped(Effect.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect.zipRight(Effect.promise(() => handler(msg.service, msg.meta, msg.json)), msg.done_))));
359
- Effect.runFork(drainLoop);
368
+ let drainLoop = Effect$1.scoped(Effect$1.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect$1.ensuring(Effect$1.catchAll(Effect.tryPromise(e => e, () => handler(msg.service, msg.meta, msg.json)), err => Effect$1.sync(() => {
369
+ console.error("[LocalBus] subscriber handler failed:", err);
370
+ })), msg.done_))));
371
+ Effect$1.runFork(drainLoop);
360
372
  };
361
373
  let subscribeToEventStream = topicName => {
362
374
  let h = eventHubs.contents[topicName];
@@ -368,10 +380,10 @@ function Make($star) {
368
380
  eventHubs.contents[topicName] = h$1;
369
381
  hub = h$1;
370
382
  }
371
- return Effect.map(Effect.acquireRelease(Effect.flatMap(Effect.sync(() => {
383
+ return Effect$1.map(Effect$1.acquireRelease(Effect$1.flatMap(Effect$1.sync(() => {
372
384
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
373
385
  subscriberCounts.contents[topicName] = n + 1 | 0;
374
- }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect.zipRight(Effect.sync(() => {
386
+ }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect$1.zipRight(Effect$1.sync(() => {
375
387
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 1);
376
388
  subscriberCounts.contents[topicName] = n - 1 | 0;
377
389
  }), Queue.shutdown(queue))), queue => Stream.fromQueue(queue));
@@ -389,17 +401,17 @@ function Make($star) {
389
401
  if (n === 0) {
390
402
  return;
391
403
  }
392
- let allDone = Effect.runSync(Deferred.make());
404
+ let allDone = Effect$1.runSync(Deferred.make());
393
405
  let remaining = {
394
406
  contents: n
395
407
  };
396
- let done_ = Effect.flatMap(Effect.sync(() => {
408
+ let done_ = Effect$1.flatMap(Effect$1.sync(() => {
397
409
  remaining.contents = remaining.contents - 1 | 0;
398
410
  }), () => {
399
411
  if (remaining.contents === 0) {
400
- return Effect.map(Deferred.succeed(allDone, undefined), param => {});
412
+ return Effect$1.map(Deferred.succeed(allDone, undefined), param => {});
401
413
  } else {
402
- return Effect.succeed();
414
+ return Effect$1.succeed();
403
415
  }
404
416
  });
405
417
  let msg = {
@@ -408,10 +420,10 @@ function Make($star) {
408
420
  json: json,
409
421
  done_: done_
410
422
  };
411
- let publishAndWait = Effect.zipRight(Effect.sync(() => {
412
- Effect.runSync(PubSub.publish(hub$1, msg));
423
+ let publishAndWait = Effect$1.zipRight(Effect$1.sync(() => {
424
+ Effect$1.runSync(PubSub.publish(hub$1, msg));
413
425
  }), Deferred.await(allDone));
414
- await Effect.runPromise(publishAndWait);
426
+ await Effect$1.runPromise(publishAndWait);
415
427
  };
416
428
  let dispatchCommand = async (channelName, json) => {
417
429
  let handler = commandHandlers.contents[channelName];
@@ -440,7 +452,10 @@ function Make($star) {
440
452
  let pending = queue.contents;
441
453
  queue.contents = [];
442
454
  pending.forEach(json => {
443
- handler(json, undefined);
455
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
456
+ console.error("[LocalBus] parked command handler failed:", e);
457
+ return Promise.resolve();
458
+ });
444
459
  });
445
460
  };
446
461
  let registerQueryDb = (name, ops) => {
@@ -480,7 +495,10 @@ function Make($star) {
480
495
  contents: {}
481
496
  };
482
497
  let makeFireAndForgetHandler = handler => ((param, param$1, json) => {
483
- handler(json, undefined);
498
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
499
+ console.error("[LocalBus] fire-and-forget handler failed:", e);
500
+ return Promise.resolve();
501
+ });
484
502
  return Promise.resolve();
485
503
  });
486
504
  let registerEventCollectorHandler = (ecName, handler) => {
@@ -501,10 +519,10 @@ function Make($star) {
501
519
  eventCollectorPendingTopics.contents[ecName] = pending;
502
520
  };
503
521
  let reset = () => {
504
- let shutdownAll = Effect.map(Effect.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
522
+ let shutdownAll = Effect$1.map(Effect$1.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
505
523
  concurrency: "unbounded"
506
524
  }), param => {});
507
- Effect.runSync(shutdownAll);
525
+ Effect$1.runSync(shutdownAll);
508
526
  eventHubs.contents = {};
509
527
  subscriberCounts.contents = {};
510
528
  commandHandlers.contents = {};
@@ -570,7 +588,7 @@ function MakeSilent($star) {
570
588
  let dcbEventLogReadRegistry = {
571
589
  contents: {}
572
590
  };
573
- let makeHub = () => Effect.runSync(PubSub.unbounded());
591
+ let makeHub = () => Effect$1.runSync(PubSub.unbounded());
574
592
  let subscribeToEvents = (topicName, handler) => {
575
593
  let h = eventHubs.contents[topicName];
576
594
  let hub;
@@ -583,8 +601,10 @@ function MakeSilent($star) {
583
601
  }
584
602
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
585
603
  subscriberCounts.contents[topicName] = n + 1 | 0;
586
- let drainLoop = Effect.scoped(Effect.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect.zipRight(Effect.promise(() => handler(msg.service, msg.meta, msg.json)), msg.done_))));
587
- Effect.runFork(drainLoop);
604
+ let drainLoop = Effect$1.scoped(Effect$1.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect$1.ensuring(Effect$1.catchAll(Effect.tryPromise(e => e, () => handler(msg.service, msg.meta, msg.json)), err => Effect$1.sync(() => {
605
+ console.error("[LocalBus] subscriber handler failed:", err);
606
+ })), msg.done_))));
607
+ Effect$1.runFork(drainLoop);
588
608
  };
589
609
  let subscribeToEventStream = topicName => {
590
610
  let h = eventHubs.contents[topicName];
@@ -596,10 +616,10 @@ function MakeSilent($star) {
596
616
  eventHubs.contents[topicName] = h$1;
597
617
  hub = h$1;
598
618
  }
599
- return Effect.map(Effect.acquireRelease(Effect.flatMap(Effect.sync(() => {
619
+ return Effect$1.map(Effect$1.acquireRelease(Effect$1.flatMap(Effect$1.sync(() => {
600
620
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
601
621
  subscriberCounts.contents[topicName] = n + 1 | 0;
602
- }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect.zipRight(Effect.sync(() => {
622
+ }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect$1.zipRight(Effect$1.sync(() => {
603
623
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 1);
604
624
  subscriberCounts.contents[topicName] = n - 1 | 0;
605
625
  }), Queue.shutdown(queue))), queue => Stream.fromQueue(queue));
@@ -617,17 +637,17 @@ function MakeSilent($star) {
617
637
  if (n === 0) {
618
638
  return;
619
639
  }
620
- let allDone = Effect.runSync(Deferred.make());
640
+ let allDone = Effect$1.runSync(Deferred.make());
621
641
  let remaining = {
622
642
  contents: n
623
643
  };
624
- let done_ = Effect.flatMap(Effect.sync(() => {
644
+ let done_ = Effect$1.flatMap(Effect$1.sync(() => {
625
645
  remaining.contents = remaining.contents - 1 | 0;
626
646
  }), () => {
627
647
  if (remaining.contents === 0) {
628
- return Effect.map(Deferred.succeed(allDone, undefined), param => {});
648
+ return Effect$1.map(Deferred.succeed(allDone, undefined), param => {});
629
649
  } else {
630
- return Effect.succeed();
650
+ return Effect$1.succeed();
631
651
  }
632
652
  });
633
653
  let msg = {
@@ -636,10 +656,10 @@ function MakeSilent($star) {
636
656
  json: json,
637
657
  done_: done_
638
658
  };
639
- let publishAndWait = Effect.zipRight(Effect.sync(() => {
640
- Effect.runSync(PubSub.publish(hub$1, msg));
659
+ let publishAndWait = Effect$1.zipRight(Effect$1.sync(() => {
660
+ Effect$1.runSync(PubSub.publish(hub$1, msg));
641
661
  }), Deferred.await(allDone));
642
- await Effect.runPromise(publishAndWait);
662
+ await Effect$1.runPromise(publishAndWait);
643
663
  };
644
664
  let dispatchCommand = async (channelName, json) => {
645
665
  let handler = commandHandlers.contents[channelName];
@@ -668,7 +688,10 @@ function MakeSilent($star) {
668
688
  let pending = queue.contents;
669
689
  queue.contents = [];
670
690
  pending.forEach(json => {
671
- handler(json, undefined);
691
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
692
+ console.error("[LocalBus] parked command handler failed:", e);
693
+ return Promise.resolve();
694
+ });
672
695
  });
673
696
  };
674
697
  let registerQueryDb = (name, ops) => {
@@ -708,7 +731,10 @@ function MakeSilent($star) {
708
731
  contents: {}
709
732
  };
710
733
  let makeFireAndForgetHandler = handler => ((param, param$1, json) => {
711
- handler(json, undefined);
734
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
735
+ console.error("[LocalBus] fire-and-forget handler failed:", e);
736
+ return Promise.resolve();
737
+ });
712
738
  return Promise.resolve();
713
739
  });
714
740
  let registerEventCollectorHandler = (ecName, handler) => {
@@ -729,10 +755,10 @@ function MakeSilent($star) {
729
755
  eventCollectorPendingTopics.contents[ecName] = pending;
730
756
  };
731
757
  let reset = () => {
732
- let shutdownAll = Effect.map(Effect.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
758
+ let shutdownAll = Effect$1.map(Effect$1.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
733
759
  concurrency: "unbounded"
734
760
  }), param => {});
735
- Effect.runSync(shutdownAll);
761
+ Effect$1.runSync(shutdownAll);
736
762
  eventHubs.contents = {};
737
763
  subscriberCounts.contents = {};
738
764
  commandHandlers.contents = {};
@@ -799,7 +825,7 @@ function MakeBounded(C) {
799
825
  let dcbEventLogReadRegistry = {
800
826
  contents: {}
801
827
  };
802
- let makeHub = () => Effect.runSync(PubSub.bounded(capacity));
828
+ let makeHub = () => Effect$1.runSync(PubSub.bounded(capacity));
803
829
  let subscribeToEvents = (topicName, handler) => {
804
830
  let h = eventHubs.contents[topicName];
805
831
  let hub;
@@ -812,8 +838,10 @@ function MakeBounded(C) {
812
838
  }
813
839
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
814
840
  subscriberCounts.contents[topicName] = n + 1 | 0;
815
- let drainLoop = Effect.scoped(Effect.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect.zipRight(Effect.promise(() => handler(msg.service, msg.meta, msg.json)), msg.done_))));
816
- Effect.runFork(drainLoop);
841
+ let drainLoop = Effect$1.scoped(Effect$1.flatMap(PubSub.subscribe(hub), queue => Stream.runForEach(Stream.fromQueue(queue), msg => Effect$1.ensuring(Effect$1.catchAll(Effect.tryPromise(e => e, () => handler(msg.service, msg.meta, msg.json)), err => Effect$1.sync(() => {
842
+ console.error("[LocalBus] subscriber handler failed:", err);
843
+ })), msg.done_))));
844
+ Effect$1.runFork(drainLoop);
817
845
  };
818
846
  let subscribeToEventStream = topicName => {
819
847
  let h = eventHubs.contents[topicName];
@@ -825,10 +853,10 @@ function MakeBounded(C) {
825
853
  eventHubs.contents[topicName] = h$1;
826
854
  hub = h$1;
827
855
  }
828
- return Effect.map(Effect.acquireRelease(Effect.flatMap(Effect.sync(() => {
856
+ return Effect$1.map(Effect$1.acquireRelease(Effect$1.flatMap(Effect$1.sync(() => {
829
857
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 0);
830
858
  subscriberCounts.contents[topicName] = n + 1 | 0;
831
- }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect.zipRight(Effect.sync(() => {
859
+ }), () => PubSub.subscribe(hub)), (queue, _exit) => Effect$1.zipRight(Effect$1.sync(() => {
832
860
  let n = Stdlib_Option.getOr(subscriberCounts.contents[topicName], 1);
833
861
  subscriberCounts.contents[topicName] = n - 1 | 0;
834
862
  }), Queue.shutdown(queue))), queue => Stream.fromQueue(queue));
@@ -845,17 +873,17 @@ function MakeBounded(C) {
845
873
  if (n === 0) {
846
874
  return;
847
875
  }
848
- let allDone = Effect.runSync(Deferred.make());
876
+ let allDone = Effect$1.runSync(Deferred.make());
849
877
  let remaining = {
850
878
  contents: n
851
879
  };
852
- let done_ = Effect.flatMap(Effect.sync(() => {
880
+ let done_ = Effect$1.flatMap(Effect$1.sync(() => {
853
881
  remaining.contents = remaining.contents - 1 | 0;
854
882
  }), () => {
855
883
  if (remaining.contents === 0) {
856
- return Effect.map(Deferred.succeed(allDone, undefined), param => {});
884
+ return Effect$1.map(Deferred.succeed(allDone, undefined), param => {});
857
885
  } else {
858
- return Effect.succeed();
886
+ return Effect$1.succeed();
859
887
  }
860
888
  });
861
889
  let msg = {
@@ -864,8 +892,8 @@ function MakeBounded(C) {
864
892
  json: json,
865
893
  done_: done_
866
894
  };
867
- let publishAndWait = Effect.flatMap(PubSub.publish(Primitive_option.valFromOption(hub), msg), param => Deferred.await(allDone));
868
- await Effect.runPromise(publishAndWait);
895
+ let publishAndWait = Effect$1.flatMap(PubSub.publish(Primitive_option.valFromOption(hub), msg), param => Deferred.await(allDone));
896
+ await Effect$1.runPromise(publishAndWait);
869
897
  };
870
898
  let dispatchCommand = async (channelName, json) => {
871
899
  let handler = commandHandlers.contents[channelName];
@@ -894,7 +922,10 @@ function MakeBounded(C) {
894
922
  let pending = queue.contents;
895
923
  queue.contents = [];
896
924
  pending.forEach(json => {
897
- handler(json, undefined);
925
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
926
+ console.error("[LocalBus] parked command handler failed:", e);
927
+ return Promise.resolve();
928
+ });
898
929
  });
899
930
  };
900
931
  let registerQueryDb = (name, ops) => {
@@ -934,7 +965,10 @@ function MakeBounded(C) {
934
965
  contents: {}
935
966
  };
936
967
  let makeFireAndForgetHandler = handler => ((param, param$1, json) => {
937
- handler(json, undefined);
968
+ Stdlib_Promise.$$catch(handler(json, undefined), e => {
969
+ console.error("[LocalBus] fire-and-forget handler failed:", e);
970
+ return Promise.resolve();
971
+ });
938
972
  return Promise.resolve();
939
973
  });
940
974
  let registerEventCollectorHandler = (ecName, handler) => {
@@ -955,10 +989,10 @@ function MakeBounded(C) {
955
989
  eventCollectorPendingTopics.contents[ecName] = pending;
956
990
  };
957
991
  let reset = () => {
958
- let shutdownAll = Effect.map(Effect.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
992
+ let shutdownAll = Effect$1.map(Effect$1.all(Object.values(eventHubs.contents).map(hub => PubSub.shutdown(hub)), {
959
993
  concurrency: "unbounded"
960
994
  }), param => {});
961
- Effect.runSync(shutdownAll);
995
+ Effect$1.runSync(shutdownAll);
962
996
  eventHubs.contents = {};
963
997
  subscriberCounts.contents = {};
964
998
  commandHandlers.contents = {};
@@ -1008,4 +1042,4 @@ export {
1008
1042
  MakeSilent,
1009
1043
  MakeBounded,
1010
1044
  }
1011
- /* effect/Queue Not a pure module */
1045
+ /* Effect Not a pure module */
@@ -87,6 +87,60 @@ module Make = (Bus: LocalBus.T) => {
87
87
  allItems.contents = flattenStore(store.contents)
88
88
  }
89
89
 
90
+ // TTL parity with the SQLite backend (which filters on `expires_at`). We
91
+ // record an absolute expiry (epoch seconds, the same value SQLite stores) per
92
+ // (partition, sub-key) and drop expired entries lazily on read — a session
93
+ // without reads never spends cycles expiring, matching SQLite's read-time
94
+ // `notExpiredClause`. Previously `~ttl` was ignored entirely in memory.
95
+ let expiries: ref<dict<dict<float>>> = ref(Dict.make())
96
+ let nowSecs = () => Date.now() /. 1000.0
97
+ let recordExpiry = (id: string, subKey: string, ttl: option<int>) =>
98
+ switch ttl {
99
+ | Some(t) =>
100
+ let subExp = switch expiries.contents->Dict.get(id) {
101
+ | Some(m) => m
102
+ | None =>
103
+ let m = Dict.make()
104
+ expiries.contents->Dict.set(id, m)
105
+ m
106
+ }
107
+ subExp->Dict.set(subKey, Int.toFloat(t))
108
+ | None =>
109
+ switch expiries.contents->Dict.get(id) {
110
+ | Some(m) => m->Dict.delete(subKey)
111
+ | None => ()
112
+ }
113
+ }
114
+ let purgeExpired = () => {
115
+ let now = nowSecs()
116
+ let removedAny = ref(false)
117
+ expiries.contents
118
+ ->Dict.toArray
119
+ ->Array.forEach(((id, subExp)) =>
120
+ subExp
121
+ ->Dict.toArray
122
+ ->Array.forEach(((subKey, exp)) =>
123
+ if exp <= now {
124
+ switch store.contents->Dict.get(id) {
125
+ | Some(sm) =>
126
+ if sm->Dict.get(subKey)->Option.isSome {
127
+ sm->Dict.delete(subKey)
128
+ removedAny := true
129
+ }
130
+ if sm->Dict.keysToArray->Array.length == 0 {
131
+ store.contents->Dict.delete(id)
132
+ }
133
+ | None => ()
134
+ }
135
+ subExp->Dict.delete(subKey)
136
+ }
137
+ )
138
+ )
139
+ if removedAny.contents {
140
+ syncAll()
141
+ }
142
+ }
143
+
90
144
  let getOrCreateSubMap = (partitionKey: string): dict<JSON.t> =>
91
145
  switch store.contents->Dict.get(partitionKey) {
92
146
  | Some(m) => m
@@ -96,11 +150,15 @@ module Make = (Bus: LocalBus.T) => {
96
150
  m
97
151
  }
98
152
 
99
- let load: QueryDb.load<string, JSON.t> = async id =>
153
+ let load: QueryDb.load<string, JSON.t> = async id => {
154
+ purgeExpired()
100
155
  Ok(sortedItems(store.contents, id))
156
+ }
101
157
 
102
- let loadStream: QueryDb.loadStream<string, JSON.t> = id =>
158
+ let loadStream: QueryDb.loadStream<string, JSON.t> = id => {
159
+ purgeExpired()
103
160
  sortedItems(store.contents, id)->Stream.fromIterable
161
+ }
104
162
 
105
163
  // Entity key matches the AWS StateTopic Lambda output (Phase 1):
106
164
  // single-key tables → partition value; composite tables → `pk-sk`.
@@ -129,20 +187,22 @@ module Make = (Bus: LocalBus.T) => {
129
187
  Bus.publishStateChange(~name, ~descriptor)
130
188
  }
131
189
 
132
- let save: QueryDb.save<string, JSON.t> = async (id, state, _saveMode, _ttl) => {
190
+ let save: QueryDb.save<string, JSON.t> = async (id, state, _saveMode, ttl) => {
133
191
  let subKey = getSubKey(state, subIdField)
134
192
  let subMap = getOrCreateSubMap(id)
135
193
  subMap->Dict.set(subKey, state)
194
+ recordExpiry(id, subKey, ttl)
136
195
  syncAll()
137
196
  publishUpdated(id, state)
138
197
  Ok()
139
198
  }
140
199
 
141
200
  let saveBatch: QueryDb.saveBatch<string, JSON.t> = async batch => {
142
- batch->Array.forEach(((id, state, _ttl)) => {
201
+ batch->Array.forEach(((id, state, ttl)) => {
143
202
  let subKey = getSubKey(state, subIdField)
144
203
  let subMap = getOrCreateSubMap(id)
145
204
  subMap->Dict.set(subKey, state)
205
+ recordExpiry(id, subKey, ttl)
146
206
  })
147
207
  syncAll()
148
208
  // Notify subscribers for each saved item
@@ -150,7 +210,33 @@ module Make = (Bus: LocalBus.T) => {
150
210
  Ok()
151
211
  }
152
212
 
153
- let count: QueryDb.count<string> = async (_id, _fieldName, inc) => Ok(inc)
213
+ // Atomic-ish counter, mirroring DynamoDB's `ADD #fieldName :inc` on key {id}:
214
+ // read the field on the partition-key item (counters are single-state, so the
215
+ // implicit sub-key ""), add `inc`, persist, and return the NEW total. The
216
+ // previous `Ok(inc)` echoed the increment and never touched the store, so the
217
+ // running total was wrong and `loadStream` never reflected the counter.
218
+ let count: QueryDb.count<string> = async (id, fieldName, inc) => {
219
+ let subMap = getOrCreateSubMap(id)
220
+ let existing = subMap->Dict.get("")->Option.flatMap(JSON.Decode.object)
221
+ let current =
222
+ existing
223
+ ->Option.flatMap(o => o->Dict.get(fieldName))
224
+ ->Option.flatMap(JSON.Decode.float)
225
+ ->Option.mapOr(0, Float.toInt)
226
+ let next = current + inc
227
+ let obj = Dict.make()
228
+ switch existing {
229
+ | Some(o) => o->Dict.toArray->Array.forEach(((k, v)) => obj->Dict.set(k, v))
230
+ | None => ()
231
+ }
232
+ obj->Dict.set("id", JSON.Encode.string(id))
233
+ obj->Dict.set(fieldName, JSON.Encode.int(next))
234
+ let newItem = JSON.Encode.object(obj)
235
+ subMap->Dict.set("", newItem)
236
+ syncAll()
237
+ publishUpdated(id, newItem)
238
+ Ok(next)
239
+ }
154
240
 
155
241
  let delete: QueryDb.delete<string> = async (id, subIdOpt) => {
156
242
  switch subIdOpt {
@@ -215,8 +301,14 @@ module Make = (Bus: LocalBus.T) => {
215
301
  }
216
302
 
217
303
  Bus.registerQueryDb(name, ops)
218
- Bus.registerQueryDbScan(name, () => allItems.contents)
219
- Bus.registerQueryDbStream(name, () => allItems.contents->Stream.fromIterable)
304
+ Bus.registerQueryDbScan(name, () => {
305
+ purgeExpired()
306
+ allItems.contents
307
+ })
308
+ Bus.registerQueryDbStream(name, () => {
309
+ purgeExpired()
310
+ allItems.contents->Stream.fromIterable
311
+ })
220
312
 
221
313
  {
222
314
  resources: [],