@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250

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 (170) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +13 -13
  3. package/src/BakedManifest.res +5 -6
  4. package/src/EmitCapabilities.res +3 -1
  5. package/src/LocalPlatformRegistry.res +3 -1
  6. package/src/LocalPlatformStart.res +7 -6
  7. package/src/LocalSeedTarget.res +3 -1
  8. package/src/Platform.res +291 -215
  9. package/src/ShellConfig.res +10 -17
  10. package/src/UiHints.res +8 -14
  11. package/src/UiSlots.res +10 -17
  12. package/src/adapter/Api/LocalEvents_Server.res +5 -3
  13. package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
  14. package/src/adapter/Auth/LocalAuth.res +11 -15
  15. package/src/adapter/Auth/UserStore.res +5 -7
  16. package/src/adapter/Backend.res +1 -2
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  18. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  19. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  20. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  21. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  22. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  23. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  24. package/src/adapter/DomainGraphQL_Server.res +35 -34
  25. package/src/adapter/DomainMCP_Server.res +45 -41
  26. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  27. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  28. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  29. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  30. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  31. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  32. package/src/adapter/LocalBus.res +12 -12
  33. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  34. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  35. package/src/adapter/LocalSideEffectHandler.res +1 -1
  36. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  37. package/src/adapter/MCP_ServerInstance.res +51 -39
  38. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  39. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  40. package/src/adapter/PlatformGraphQL_Server.res +9 -5
  41. package/src/adapter/ProjectionCheckpoint.res +8 -1
  42. package/src/adapter/ProjectionPending.res +1 -3
  43. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  44. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  45. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  46. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  47. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  48. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  49. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  50. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  51. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  52. package/src/adapter/SqliteDriver.res +16 -4
  53. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  54. package/src/components/Aggregate_Builder.res +6 -1
  55. package/src/components/Counter_Builder.res +1 -5
  56. package/src/components/DcbEventLog_Builder.res +5 -3
  57. package/src/components/ExtensionPoint_Builder.res +13 -15
  58. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  59. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  60. package/src/components/ReadModel_Builder.res +18 -20
  61. package/src/components/StateViewSlice_Builder.res +3 -2
  62. package/src/components/Task_Builder.res +12 -13
  63. package/src/reset/LocalSeedReset.res +2 -5
  64. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  65. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  66. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  67. package/src/test/TestRunner.res +1 -1
  68. package/tests/BakedManifestFilesTest.res +7 -10
  69. package/tests/LocalEventTapTest.res +26 -21
  70. package/tests/LocalPlatformStartTest.res +33 -28
  71. package/tests/PluginEventDecodeTest.res +3 -3
  72. package/tests/ShellConfigTest.res +30 -21
  73. package/tests/SplitApiFixtures.res +34 -10
  74. package/tests/SplitApiTest.res +92 -62
  75. package/tests/adapter/BackendParityTest.res +180 -130
  76. package/tests/adapter/CollectNEventsTest.res +6 -4
  77. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  78. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  79. package/tests/adapter/CounterHandlerTest.res +95 -77
  80. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  81. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  82. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  83. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  84. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  85. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  86. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  87. package/tests/adapter/EventTapTest.res +37 -20
  88. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  89. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  90. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  91. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  92. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  93. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  94. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  95. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  96. package/tests/adapter/LocalAuthTest.res +8 -16
  97. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  98. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  99. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  100. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  101. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  102. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  103. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  104. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  105. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  106. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  107. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  108. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  109. package/tests/adapter/QueryDbStorageTest.res +163 -117
  110. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  111. package/tests/adapter/QueryEngineTest.res +179 -126
  112. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  113. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  114. package/tests/adapter/SortKey_FilterTest.res +19 -3
  115. package/tests/adapter/SqliteDriverTest.res +13 -10
  116. package/tests/adapter/TaskBucketTest.res +94 -80
  117. package/tests/components/aggregate/AggregateTest.res +31 -18
  118. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  119. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  120. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  121. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  122. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  123. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  124. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  125. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  126. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  127. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  128. package/tests/components/counter/CounterFixtures.res +6 -8
  129. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  130. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  131. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  132. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  133. package/tests/components/dcb/DcbFixtures.res +4 -1
  134. package/tests/components/dcb/DcbTest.res +9 -6
  135. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  136. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  137. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  138. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  139. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  140. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  141. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  142. package/tests/components/eventlog/EventLogTest.res +20 -4
  143. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  144. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  145. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  146. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  147. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  148. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  149. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  150. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  151. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  152. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  153. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  154. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  155. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  156. package/tests/components/querydb/QueryDbTest.res +2 -4
  157. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  158. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  159. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  160. package/tests/components/scheduler/SchedulerTest.res +76 -58
  161. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  162. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  163. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  164. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  165. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  166. package/tests/components/task/TaskFixtures.res +5 -5
  167. package/tests/components/task/TaskTest.res +95 -80
  168. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  169. package/tests/plugin/UiFragments_GWT.res +6 -1
  170. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -19,7 +19,9 @@ let bob: Reventless.Identity.t = {
19
19
  provider: InMemory,
20
20
  }
21
21
 
22
- let buildContext = (headers: array<(string, string)>): ReventlessCore.Auth_Adapter.requestContext => {
22
+ let buildContext = (
23
+ headers: array<(string, string)>,
24
+ ): ReventlessCore.Auth_Adapter.requestContext => {
23
25
  headers: Dict.fromArray(headers),
24
26
  }
25
27
 
@@ -127,9 +129,7 @@ testPromise("authenticate accepts a valid Bearer token", async () => {
127
129
  | Ok(t) => t
128
130
  | Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
129
131
  }
130
- let result = await LocalAuth.authenticate(
131
- buildContext([("authorization", "Bearer " ++ token)]),
132
- )
132
+ let result = await LocalAuth.authenticate(buildContext([("authorization", "Bearer " ++ token)]))
133
133
  switch result {
134
134
  | Authenticated(identity) =>
135
135
  expect(identity.username)->toEqual("bob")
@@ -256,11 +256,7 @@ testPromise("a narrowed token remembers the choice and what it gave up", async (
256
256
  // the failure that would hand a tampering client everything it asked for.
257
257
  testPromise("a login naming a role the user does not hold is REFUSED", async () => {
258
258
  carolLoggedIn()
259
- switch await LocalAuth.Login.issue(
260
- ~username="carol",
261
- ~password="carol-pw",
262
- ~activeRole="Admin",
263
- ) {
259
+ switch await LocalAuth.Login.issue(~username="carol", ~password="carol-pw", ~activeRole="Admin") {
264
260
  | Ok(_) => JsError.throwWithMessage("expected a request to widen to be refused")
265
261
  | Error(msg) => expect(msg->String.includes("Admin"))->toEqual(true)
266
262
  }
@@ -275,13 +271,8 @@ testPromise("narrowing cannot smuggle a group in through the claims bag", async
275
271
  claims: Dict.fromArray([("availableRoles", "Admin,Fulfilment,Shopper")]),
276
272
  }
277
273
  LocalAuth.Login.setCredentials(~username="carol", ~password="carol-pw", ~identity=withClaims)
278
- switch await LocalAuth.Login.issue(
279
- ~username="carol",
280
- ~password="carol-pw",
281
- ~activeRole="Admin",
282
- ) {
283
- | Ok(_) =>
284
- JsError.throwWithMessage("expected membership to be judged by groups, not by a claim")
274
+ switch await LocalAuth.Login.issue(~username="carol", ~password="carol-pw", ~activeRole="Admin") {
275
+ | Ok(_) => JsError.throwWithMessage("expected membership to be judged by groups, not by a claim")
285
276
  | Error(_) => expect(true)->toEqual(true)
286
277
  }
287
278
  })
@@ -298,9 +289,7 @@ testPromise("a narrowed token authenticates as the narrowed identity", async ()
298
289
  | Ok(t) => t
299
290
  | Error(e) => JsError.throwWithMessage(e)
300
291
  }
301
- let result = await LocalAuth.authenticate(
302
- buildContext([("authorization", "Bearer " ++ token)]),
303
- )
292
+ let result = await LocalAuth.authenticate(buildContext([("authorization", "Bearer " ++ token)]))
304
293
  switch result {
305
294
  | Authenticated(identity) => expect(identity.groups)->toEqual(["Shopper"])
306
295
  | _ => JsError.throwWithMessage("expected the narrowed token to authenticate")
@@ -408,32 +397,35 @@ describe("the conformance table, minted locally", () => {
408
397
  requested,
409
398
  expected,
410
399
  }) =>
411
- testPromise(label, async () => {
412
- resetAll()
413
- LocalAuth.Login.setCredentials(
414
- ~username="conformance",
415
- ~password="pw",
416
- ~identity={
417
- userId: "u-conformance",
418
- username: "conformance",
419
- groups: membership,
420
- provider: InMemory,
421
- },
422
- )
423
- let minted = await LocalAuth.Login.issue(
424
- ~username="conformance",
425
- ~password="pw",
426
- ~activeRole=?requested,
427
- )
428
- switch (minted, expected) {
429
- | (Ok(token), Some(groups)) => expect(decodeOrThrow(token).groups)->toEqual(groups)
430
- // `None` means refused — not honoured, not ignored, and not reduced to the
431
- // empty set. A token minted here at all would be the vulnerability.
432
- | (Ok(_), None) => JsError.throwWithMessage("expected the request to be refused")
433
- | (Error(msg), Some(_)) => JsError.throwWithMessage("expected a token, got: " ++ msg)
434
- | (Error(_), None) => expect(true)->toEqual(true)
435
- }
436
- })
400
+ testPromise(
401
+ label,
402
+ async () => {
403
+ resetAll()
404
+ LocalAuth.Login.setCredentials(
405
+ ~username="conformance",
406
+ ~password="pw",
407
+ ~identity={
408
+ userId: "u-conformance",
409
+ username: "conformance",
410
+ groups: membership,
411
+ provider: InMemory,
412
+ },
413
+ )
414
+ let minted = await LocalAuth.Login.issue(
415
+ ~username="conformance",
416
+ ~password="pw",
417
+ ~activeRole=?requested,
418
+ )
419
+ switch (minted, expected) {
420
+ | (Ok(token), Some(groups)) => expect(decodeOrThrow(token).groups)->toEqual(groups)
421
+ // `None` means refused — not honoured, not ignored, and not reduced to the
422
+ // empty set. A token minted here at all would be the vulnerability.
423
+ | (Ok(_), None) => JsError.throwWithMessage("expected the request to be refused")
424
+ | (Error(msg), Some(_)) => JsError.throwWithMessage("expected a token, got: " ++ msg)
425
+ | (Error(_), None) => expect(true)->toEqual(true)
426
+ }
427
+ },
428
+ )
437
429
  )
438
430
  })
439
431
 
@@ -453,11 +445,11 @@ describe("Login token secret", () => {
453
445
  let first = LocalAuth.Login._resolveIn(~dir)
454
446
  // A second call with no memoised value is what the next process does.
455
447
  let second = LocalAuth.Login._resolveIn(~dir)
456
- expect((first == second, NodeFs.existsSync(secretPath(dir)), String.length(first) >= 16))->toEqual((
457
- true,
458
- true,
459
- true,
460
- ))
448
+ expect((
449
+ first == second,
450
+ NodeFs.existsSync(secretPath(dir)),
451
+ String.length(first) >= 16,
452
+ ))->toEqual((true, true, true))
461
453
  })
462
454
 
463
455
  testSync("reads a secret a previous boot left behind", () => {
@@ -5,7 +5,9 @@
5
5
 
6
6
  open JestGlobals
7
7
 
8
- let buildContext = (headers: array<(string, string)>): ReventlessCore.Auth_Adapter.requestContext => {
8
+ let buildContext = (
9
+ headers: array<(string, string)>,
10
+ ): ReventlessCore.Auth_Adapter.requestContext => {
9
11
  headers: Dict.fromArray(headers),
10
12
  }
11
13
 
@@ -22,9 +24,7 @@ testPromise("no X-User, no X-Groups → defaultUser (in-memory dev convenience)"
22
24
 
23
25
  testPromise("X-User: admin resolves built-in admin identity", async () => {
24
26
  LocalAuth.resetUsers()
25
- let result = await LocalAuth.authenticate(
26
- buildContext([("x-user", "admin")]),
27
- )
27
+ let result = await LocalAuth.authenticate(buildContext([("x-user", "admin")]))
28
28
  switch result {
29
29
  | Authenticated(identity) =>
30
30
  expect(identity.username)->toEqual("admin")
@@ -35,9 +35,7 @@ testPromise("X-User: admin resolves built-in admin identity", async () => {
35
35
 
36
36
  testPromise("X-User: user resolves built-in default identity", async () => {
37
37
  LocalAuth.resetUsers()
38
- let result = await LocalAuth.authenticate(
39
- buildContext([("x-user", "user")]),
40
- )
38
+ let result = await LocalAuth.authenticate(buildContext([("x-user", "user")]))
41
39
  switch result {
42
40
  | Authenticated(identity) =>
43
41
  expect(identity.username)->toEqual("user")
@@ -48,9 +46,7 @@ testPromise("X-User: user resolves built-in default identity", async () => {
48
46
 
49
47
  testPromise("unknown X-User falls back to Anonymous", async () => {
50
48
  LocalAuth.resetUsers()
51
- let result = await LocalAuth.authenticate(
52
- buildContext([("x-user", "unknown")]),
53
- )
49
+ let result = await LocalAuth.authenticate(buildContext([("x-user", "unknown")]))
54
50
  switch result {
55
51
  | Anonymous => ()
56
52
  | _ => JsError.throwWithMessage("expected Anonymous")
@@ -72,9 +68,7 @@ testPromise("X-Groups overrides the resolved identity's groups", async () => {
72
68
 
73
69
  testPromise("X-Groups alone yields defaultUser tagged with the override groups", async () => {
74
70
  LocalAuth.resetUsers()
75
- let result = await LocalAuth.authenticate(
76
- buildContext([("x-groups", "Tester")]),
77
- )
71
+ let result = await LocalAuth.authenticate(buildContext([("x-groups", "Tester")]))
78
72
  switch result {
79
73
  | Authenticated(identity) =>
80
74
  expect(identity.userId)->toEqual("local-user")
@@ -94,9 +88,7 @@ testPromise("registerUser injects a custom identity", async () => {
94
88
  provider: InMemory,
95
89
  },
96
90
  )
97
- let result = await LocalAuth.authenticate(
98
- buildContext([("x-user", "alice")]),
99
- )
91
+ let result = await LocalAuth.authenticate(buildContext([("x-user", "alice")]))
100
92
  switch result {
101
93
  | Authenticated(identity) =>
102
94
  expect(identity.userId)->toEqual("alice-id")
@@ -4,7 +4,6 @@
4
4
 
5
5
  open JestGlobals
6
6
 
7
-
8
7
  let resetAll = () => {
9
8
  LocalAuth.resetUsers()
10
9
  LocalAuth.Login.resetStore()
@@ -115,8 +114,7 @@ testPromise("load() with no args + no default file returns Empty", async () => {
115
114
  // no such file exists.)
116
115
  switch UserStore.load() {
117
116
  | Ok(Empty) => ()
118
- | Ok(DefaultFile(_)) =>
119
- // If a .reventless/users.yaml happens to exist in CI, accept it.
117
+ | Ok(DefaultFile(_)) => // If a .reventless/users.yaml happens to exist in CI, accept it.
120
118
  ()
121
119
  | Ok(_) => JsError.throwWithMessage("expected Empty or DefaultFile")
122
120
  | Error(msg) => JsError.throwWithMessage("load failed: " ++ msg)
@@ -152,10 +150,7 @@ testPromise("inline users win over a file path", async () => {
152
150
 
153
151
  testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
154
152
  resetAll()
155
- let _ = UserStore.load(
156
- ~users=[{username: "x", password: "x", groups: []}],
157
- (),
158
- )
153
+ let _ = UserStore.load(~users=[{username: "x", password: "x", groups: []}], ())
159
154
  // Second call should not crash even if .reventless/users.yaml doesn't
160
155
  // exist; resolved=true prevents the default-discovery branch.
161
156
  UserStore.autoLoadOnce()
@@ -21,137 +21,207 @@ let defaultMeta: Reventless.Message.meta = {
21
21
 
22
22
  describe("LocalBus bounded PubSub (Phase G)", () => {
23
23
  describe("basic delivery", () => {
24
- testPromise("delivers a message to a subscriber in bounded mode", async () => {
25
- module TestBus = LocalBus.MakeBounded({let capacity = 10})
26
- let delivered = ref(false)
27
- TestBus.subscribeToEvents("T", async (_, _, _) => {
28
- delivered := true
29
- })
30
- let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
31
- expect(delivered.contents)->toBe(true)
32
- })
24
+ testPromise(
25
+ "delivers a message to a subscriber in bounded mode",
26
+ async () => {
27
+ module TestBus = LocalBus.MakeBounded({
28
+ let capacity = 10
29
+ })
30
+ let delivered = ref(false)
31
+ TestBus.subscribeToEvents(
32
+ "T",
33
+ async (_, _, _) => {
34
+ delivered := true
35
+ },
36
+ )
37
+ let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
38
+ expect(delivered.contents)->toBe(true)
39
+ },
40
+ )
33
41
 
34
- testPromise("bounded mode with no subscribers returns immediately", async () => {
35
- module TestBus = LocalBus.MakeBounded({let capacity = 5})
36
- // No subscribers — should not hang
37
- await TestBus.publishEvent("empty-topic", "svc", defaultMeta, JSON.Null)
38
- })
42
+ testPromise(
43
+ "bounded mode with no subscribers returns immediately",
44
+ async () => {
45
+ module TestBus = LocalBus.MakeBounded({
46
+ let capacity = 5
47
+ })
48
+ // No subscribers — should not hang
49
+ await TestBus.publishEvent("empty-topic", "svc", defaultMeta, JSON.Null)
50
+ },
51
+ )
39
52
 
40
- testPromise("passes service, meta, and json to the subscriber handler", async () => {
41
- module TestBus = LocalBus.MakeBounded({let capacity = 10})
42
- let capturedService = ref("")
43
- let capturedJson = ref(JSON.Null)
44
- TestBus.subscribeToEvents("T", async (svc, _meta, json) => {
45
- capturedService := svc
46
- capturedJson := json
47
- })
48
- let payload = JSON.parseOrThrow("{\"x\":42}")
49
- let _ = await TestBus.publishEvent("T", "my-service", defaultMeta, payload)
50
- expect(capturedService.contents)->toBe("my-service")
51
- expect(capturedJson.contents)->toEqual(payload)
52
- })
53
+ testPromise(
54
+ "passes service, meta, and json to the subscriber handler",
55
+ async () => {
56
+ module TestBus = LocalBus.MakeBounded({
57
+ let capacity = 10
58
+ })
59
+ let capturedService = ref("")
60
+ let capturedJson = ref(JSON.Null)
61
+ TestBus.subscribeToEvents(
62
+ "T",
63
+ async (svc, _meta, json) => {
64
+ capturedService := svc
65
+ capturedJson := json
66
+ },
67
+ )
68
+ let payload = JSON.parseOrThrow("{\"x\":42}")
69
+ let _ = await TestBus.publishEvent("T", "my-service", defaultMeta, payload)
70
+ expect(capturedService.contents)->toBe("my-service")
71
+ expect(capturedJson.contents)->toEqual(payload)
72
+ },
73
+ )
53
74
  })
54
75
 
55
76
  describe("fan-out", () => {
56
- testPromise("delivers to all subscribers on the same topic", async () => {
57
- module TestBus = LocalBus.MakeBounded({let capacity = 10})
58
- let count = ref(0)
59
- TestBus.subscribeToEvents("T", async (_, _, _) => {
60
- count := count.contents + 1
61
- })
62
- TestBus.subscribeToEvents("T", async (_, _, _) => {
63
- count := count.contents + 1
64
- })
65
- TestBus.subscribeToEvents("T", async (_, _, _) => {
66
- count := count.contents + 1
67
- })
68
- let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
69
- expect(count.contents)->toBe(3)
70
- })
77
+ testPromise(
78
+ "delivers to all subscribers on the same topic",
79
+ async () => {
80
+ module TestBus = LocalBus.MakeBounded({
81
+ let capacity = 10
82
+ })
83
+ let count = ref(0)
84
+ TestBus.subscribeToEvents(
85
+ "T",
86
+ async (_, _, _) => {
87
+ count := count.contents + 1
88
+ },
89
+ )
90
+ TestBus.subscribeToEvents(
91
+ "T",
92
+ async (_, _, _) => {
93
+ count := count.contents + 1
94
+ },
95
+ )
96
+ TestBus.subscribeToEvents(
97
+ "T",
98
+ async (_, _, _) => {
99
+ count := count.contents + 1
100
+ },
101
+ )
102
+ let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
103
+ expect(count.contents)->toBe(3)
104
+ },
105
+ )
71
106
 
72
- testPromise("topic isolation preserved in bounded mode", async () => {
73
- module TestBus = LocalBus.MakeBounded({let capacity = 10})
74
- let countA = ref(0)
75
- let countB = ref(0)
76
- TestBus.subscribeToEvents("topic-a", async (_, _, _) => {
77
- countA := countA.contents + 1
78
- })
79
- TestBus.subscribeToEvents("topic-b", async (_, _, _) => {
80
- countB := countB.contents + 1
81
- })
82
- let _ = await TestBus.publishEvent("topic-a", "svc", defaultMeta, JSON.Null)
83
- expect(countA.contents)->toBe(1)
84
- expect(countB.contents)->toBe(0)
85
- })
107
+ testPromise(
108
+ "topic isolation preserved in bounded mode",
109
+ async () => {
110
+ module TestBus = LocalBus.MakeBounded({
111
+ let capacity = 10
112
+ })
113
+ let countA = ref(0)
114
+ let countB = ref(0)
115
+ TestBus.subscribeToEvents(
116
+ "topic-a",
117
+ async (_, _, _) => {
118
+ countA := countA.contents + 1
119
+ },
120
+ )
121
+ TestBus.subscribeToEvents(
122
+ "topic-b",
123
+ async (_, _, _) => {
124
+ countB := countB.contents + 1
125
+ },
126
+ )
127
+ let _ = await TestBus.publishEvent("topic-a", "svc", defaultMeta, JSON.Null)
128
+ expect(countA.contents)->toBe(1)
129
+ expect(countB.contents)->toBe(0)
130
+ },
131
+ )
86
132
  })
87
133
 
88
134
  describe("backpressure", () => {
89
- testPromise("publisher suspends when subscriber queue is full (capacity=1)", async () => {
90
- // capacity=1: subscriber queue can buffer 1 message while the subscriber is processing.
91
- // Timeline:
92
- // p1 publish: drain fiber D has a pending Queue.take msg1 goes to D directly
93
- // p2 publish: no pending take (D is processing msg1, blocked on gate) → msg2 buffered
94
- // p3 publish: queue is full (msg2 there, capacity=1)F3 SUSPENDS
95
- // After opening gate: msg1 processed done_1 D takes msg2 F3 wakes, delivers msg3
96
- module TestBus = LocalBus.MakeBounded({let capacity = 1})
97
- let processedCount = ref(0)
98
- // Gate Deferred: subscriber blocks until explicitly opened
99
- let gate: Deferred.t<unit, unit> = Deferred.make()->Effect.runSync
100
- TestBus.subscribeToEvents("T", async (_, _, _) => {
101
- let _ = await Deferred.await_(gate)->Effect.runPromise
102
- processedCount := processedCount.contents + 1
103
- })
104
- // Start 3 publishes without awaiting.
105
- // p1: D's pending Queue.take resolves with msg1; D starts processing (blocks on gate)
106
- // p2: msg2 buffered in queue (capacity=1, fits since D already took msg1)
107
- // p3: queue full → F3 fiber suspends inside PubSub.publish
108
- let p1 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
109
- let p2 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
110
- let p3 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
111
- // Tick 1: drain fiber D runs, takes msg1, calls handler → handler blocks on gate
112
- let _ = await Promise.resolve()
113
- // Gate not open yet no messages fully processed
114
- expect(processedCount.contents)->toBe(0)
115
- // Open gate: subscriber can now process all pending messages
116
- let _ = Deferred.succeed(gate, ())->Effect.runSync
117
- // Await each publish promise; by the time all resolve, all 3 handlers have run
118
- let _ = await p1
119
- let _ = await p2
120
- let _ = await p3
121
- expect(processedCount.contents)->toBe(3)
122
- })
135
+ testPromise(
136
+ "publisher suspends when subscriber queue is full (capacity=1)",
137
+ async () => {
138
+ // capacity=1: subscriber queue can buffer 1 message while the subscriber is processing.
139
+ // Timeline:
140
+ // p1 publish: drain fiber D has a pending Queue.take msg1 goes to D directly
141
+ // p2 publish: no pending take (D is processing msg1, blocked on gate) msg2 buffered
142
+ // p3 publish: queue is full (msg2 there, capacity=1) → F3 SUSPENDS
143
+ // After opening gate: msg1 processed → done_1 → D takes msg2 → F3 wakes, delivers msg3
144
+ module TestBus = LocalBus.MakeBounded({
145
+ let capacity = 1
146
+ })
147
+ let processedCount = ref(0)
148
+ // Gate Deferred: subscriber blocks until explicitly opened
149
+ let gate: Deferred.t<unit, unit> = Deferred.make()->Effect.runSync
150
+ TestBus.subscribeToEvents(
151
+ "T",
152
+ async (_, _, _) => {
153
+ let _ = await Deferred.await_(gate)->Effect.runPromise
154
+ processedCount := processedCount.contents + 1
155
+ },
156
+ )
157
+ // Start 3 publishes without awaiting.
158
+ // p1: D's pending Queue.take resolves with msg1; D starts processing (blocks on gate)
159
+ // p2: msg2 buffered in queue (capacity=1, fits since D already took msg1)
160
+ // p3: queue full → F3 fiber suspends inside PubSub.publish
161
+ let p1 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
162
+ let p2 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
163
+ let p3 = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
164
+ // Tick 1: drain fiber D runs, takes msg1, calls handler → handler blocks on gate
165
+ let _ = await Promise.resolve()
166
+ // Gate not open yet — no messages fully processed
167
+ expect(processedCount.contents)->toBe(0)
168
+ // Open gate: subscriber can now process all pending messages
169
+ let _ = Deferred.succeed(gate, ())->Effect.runSync
170
+ // Await each publish promise; by the time all resolve, all 3 handlers have run
171
+ let _ = await p1
172
+ let _ = await p2
173
+ let _ = await p3
174
+ expect(processedCount.contents)->toBe(3)
175
+ },
176
+ )
123
177
  })
124
178
 
125
179
  describe("timing", () => {
126
- testPromise("bounded mode completes within 3 microtask ticks", async () => {
127
- module TestBus = LocalBus.MakeBounded({let capacity = 10})
128
- let delivered = ref(false)
129
- TestBus.subscribeToEvents("T", async (_, _, _) => {
130
- delivered := true
131
- })
132
- let pubPromise = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
133
- // Not delivered synchronously
134
- expect(delivered.contents)->toBe(false)
135
- let _ = await Promise.resolve() // tick 1
136
- let _ = await Promise.resolve() // tick 2
137
- let _ = await Promise.resolve() // tick 3 (bounded adds 1 vs unbounded's 2)
138
- let _ = await pubPromise
139
- expect(delivered.contents)->toBe(true)
140
- })
180
+ testPromise(
181
+ "bounded mode completes within 3 microtask ticks",
182
+ async () => {
183
+ module TestBus = LocalBus.MakeBounded({
184
+ let capacity = 10
185
+ })
186
+ let delivered = ref(false)
187
+ TestBus.subscribeToEvents(
188
+ "T",
189
+ async (_, _, _) => {
190
+ delivered := true
191
+ },
192
+ )
193
+ let pubPromise = TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
194
+ // Not delivered synchronously
195
+ expect(delivered.contents)->toBe(false)
196
+ let _ = await Promise.resolve() // tick 1
197
+ let _ = await Promise.resolve() // tick 2
198
+ let _ = await Promise.resolve() // tick 3 (bounded adds 1 vs unbounded's 2)
199
+ let _ = await pubPromise
200
+ expect(delivered.contents)->toBe(true)
201
+ },
202
+ )
141
203
  })
142
204
 
143
205
  describe("reset", () => {
144
- testPromise("reset shuts down bounded hubs cleanly", async () => {
145
- module TestBus = LocalBus.MakeBounded({let capacity = 5})
146
- let count = ref(0)
147
- TestBus.subscribeToEvents("T", async (_, _, _) => {
148
- count := count.contents + 1
149
- })
150
- let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
151
- expect(count.contents)->toBe(1)
152
- // reset should not throw or hang
153
- TestBus.reset()
154
- expect(count.contents)->toBe(1) // count unchanged by reset itself
155
- })
206
+ testPromise(
207
+ "reset shuts down bounded hubs cleanly",
208
+ async () => {
209
+ module TestBus = LocalBus.MakeBounded({
210
+ let capacity = 5
211
+ })
212
+ let count = ref(0)
213
+ TestBus.subscribeToEvents(
214
+ "T",
215
+ async (_, _, _) => {
216
+ count := count.contents + 1
217
+ },
218
+ )
219
+ let _ = await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
220
+ expect(count.contents)->toBe(1)
221
+ // reset should not throw or hang
222
+ TestBus.reset()
223
+ expect(count.contents)->toBe(1) // count unchanged by reset itself
224
+ },
225
+ )
156
226
  })
157
227
  })