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

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 (174) hide show
  1. package/CHANGELOG.md +30 -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 +12 -16
  15. package/src/adapter/Auth/LocalAuth.res.mjs +2 -1
  16. package/src/adapter/Auth/UserStore.res +13 -40
  17. package/src/adapter/Auth/UserStore.res.mjs +11 -67
  18. package/src/adapter/Backend.res +1 -2
  19. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  20. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  21. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  22. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  23. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  24. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  25. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  26. package/src/adapter/DomainGraphQL_Server.res +35 -34
  27. package/src/adapter/DomainMCP_Server.res +45 -41
  28. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  29. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  30. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  31. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  32. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  33. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  34. package/src/adapter/LocalBus.res +12 -12
  35. package/src/adapter/LocalCapabilities.res +6 -0
  36. package/src/adapter/LocalCapabilities.res.mjs +5 -2
  37. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  38. package/src/adapter/LocalSideEffectHandler.res +1 -1
  39. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  40. package/src/adapter/MCP_ServerInstance.res +51 -39
  41. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  42. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  43. package/src/adapter/PlatformGraphQL_Server.res +10 -6
  44. package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -1
  45. package/src/adapter/ProjectionCheckpoint.res +8 -1
  46. package/src/adapter/ProjectionPending.res +1 -3
  47. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  48. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  49. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  50. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  51. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  52. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  53. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  54. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  55. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  56. package/src/adapter/SqliteDriver.res +16 -4
  57. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  58. package/src/components/Aggregate_Builder.res +6 -1
  59. package/src/components/Counter_Builder.res +1 -5
  60. package/src/components/DcbEventLog_Builder.res +5 -3
  61. package/src/components/ExtensionPoint_Builder.res +13 -15
  62. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  63. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  64. package/src/components/ReadModel_Builder.res +18 -20
  65. package/src/components/StateViewSlice_Builder.res +3 -2
  66. package/src/components/Task_Builder.res +12 -13
  67. package/src/reset/LocalSeedReset.res +2 -5
  68. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  69. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  70. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  71. package/src/test/TestRunner.res +1 -1
  72. package/tests/BakedManifestFilesTest.res +7 -10
  73. package/tests/LocalEventTapTest.res +26 -21
  74. package/tests/LocalPlatformStartTest.res +33 -28
  75. package/tests/PluginEventDecodeTest.res +3 -3
  76. package/tests/ShellConfigTest.res +30 -21
  77. package/tests/SplitApiFixtures.res +34 -10
  78. package/tests/SplitApiTest.res +92 -62
  79. package/tests/adapter/BackendParityTest.res +180 -130
  80. package/tests/adapter/CollectNEventsTest.res +6 -4
  81. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  82. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  83. package/tests/adapter/CounterHandlerTest.res +95 -77
  84. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  85. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  86. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  87. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  88. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  89. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  90. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  91. package/tests/adapter/EventTapTest.res +37 -20
  92. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  93. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  94. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  95. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  96. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  97. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  98. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  99. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  100. package/tests/adapter/LocalAuthTest.res +8 -16
  101. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  102. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  103. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  104. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  105. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  106. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  107. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  108. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  109. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  110. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  111. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  112. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  113. package/tests/adapter/QueryDbStorageTest.res +163 -117
  114. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  115. package/tests/adapter/QueryEngineTest.res +179 -126
  116. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  117. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  118. package/tests/adapter/SortKey_FilterTest.res +19 -3
  119. package/tests/adapter/SqliteDriverTest.res +13 -10
  120. package/tests/adapter/TaskBucketTest.res +94 -80
  121. package/tests/components/aggregate/AggregateTest.res +31 -18
  122. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  123. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  124. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  125. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  126. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  127. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  128. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  129. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  130. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  131. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  132. package/tests/components/counter/CounterFixtures.res +6 -8
  133. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  134. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  135. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  136. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  137. package/tests/components/dcb/DcbFixtures.res +4 -1
  138. package/tests/components/dcb/DcbTest.res +9 -6
  139. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  140. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  141. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  142. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  143. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  144. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  145. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  146. package/tests/components/eventlog/EventLogTest.res +20 -4
  147. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  148. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  149. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  150. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  151. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  152. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  153. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  154. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  155. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  156. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  157. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  158. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  159. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  160. package/tests/components/querydb/QueryDbTest.res +2 -4
  161. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  162. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  163. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  164. package/tests/components/scheduler/SchedulerTest.res +76 -58
  165. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  166. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  167. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  168. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  169. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  170. package/tests/components/task/TaskFixtures.res +5 -5
  171. package/tests/components/task/TaskTest.res +95 -80
  172. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  173. package/tests/plugin/UiFragments_GWT.res +6 -1
  174. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -60,104 +60,113 @@ describe("LocalGraphQL_SubscriptionResolvers", () => {
60
60
  })
61
61
 
62
62
  describe("Source B — state-change bridge", () => {
63
- testPromise("Bus.publishStateChange → yoga PubSub subscriber receives descriptor", async () => {
64
- module TestBus = LocalBus.Make()
65
-
66
- // Wire the bridge before subscribing, so the bus callback publishes to the PubSub.
67
- LocalGraphQL_SubscriptionResolvers.bridgeSourceB(
68
- ~subscribeToStateChanges=TestBus.subscribeToStateChanges,
69
- ~readModelName="Product",
70
- ~returnTypeName="CatalogProduct",
71
- )
72
-
73
- // Start the consumer BEFORE publishing — yoga's PubSub drops messages
74
- // on topics whose AsyncIterable listener isn't registered yet.
75
- let ps = LocalGraphQL_SubscriptionResolvers.getPubSub()
76
- let iter = ps->GraphqlYoga.pubSubSubscribe("onCatalogProduct_stateChanged")
77
- let consumerPromise = startConsumer(iter, 1000)
78
- await yieldTick() // ensure listener is registered before publish
79
-
80
- // Bus payload is the change descriptor matching the AWS StateTopic Lambda
81
- // output shape: {changeKind, id, sortKeyValue?, seq, state?}.
82
- let state = JSON.Encode.object(
83
- Dict.fromArray([
84
- ("id", JSON.Encode.string("prod-1")),
85
- ("name", JSON.Encode.string("Widget")),
86
- ("updatedAt", JSON.Encode.string("2026-05-19T12:00:00Z")),
87
- ]),
88
- )
89
- let descriptor = LocalStateChangeDescriptor.make(
90
- ~changeKind="Updated",
91
- ~id="prod-1",
92
- ~state=Some(state),
93
- ~seq=LocalStateChangeDescriptor.nextSequence(),
94
- )
95
- TestBus.publishStateChange(~name="Product", ~descriptor)
96
-
97
- let received = await consumerPromise
98
- switch received->Null.toOption {
99
- | Some(msg) => expect(msg)->toEqual(descriptor)
100
- | None => expect("onCatalogProduct_stateChanged: timed out")->toBe("received")
101
- }
102
- })
103
-
104
- testPromise("state change for unregistered ReadModel → no publish", async () => {
105
- module TestBus = LocalBus.Make()
106
-
107
- LocalGraphQL_SubscriptionResolvers.bridgeSourceB(
108
- ~subscribeToStateChanges=TestBus.subscribeToStateChanges,
109
- ~readModelName="Product",
110
- ~returnTypeName="CatalogProduct",
111
- )
112
-
113
- let ps = LocalGraphQL_SubscriptionResolvers.getPubSub()
114
- let iter = ps->GraphqlYoga.pubSubSubscribe("onCatalogProduct_stateChanged")
115
- let consumerPromise = startConsumer(iter, 300)
116
- await yieldTick()
117
-
118
- // Publish for a DIFFERENT ReadModel — should NOT reach our topic.
119
- let foreignDescriptor = LocalStateChangeDescriptor.make(
120
- ~changeKind="Updated",
121
- ~id="cat-1",
122
- ~state=Some(JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("cat-1"))]))),
123
- ~seq=LocalStateChangeDescriptor.nextSequence(),
124
- )
125
- TestBus.publishStateChange(~name="Category", ~descriptor=foreignDescriptor)
126
-
127
- let received = await consumerPromise
128
- expect(received->Null.toOption)->toEqual(None)
129
- })
63
+ testPromise(
64
+ "Bus.publishStateChange yoga PubSub subscriber receives descriptor",
65
+ async () => {
66
+ module TestBus = LocalBus.Make()
67
+
68
+ // Wire the bridge before subscribing, so the bus callback publishes to the PubSub.
69
+ LocalGraphQL_SubscriptionResolvers.bridgeSourceB(
70
+ ~subscribeToStateChanges=TestBus.subscribeToStateChanges,
71
+ ~readModelName="Product",
72
+ ~returnTypeName="CatalogProduct",
73
+ )
74
+
75
+ // Start the consumer BEFORE publishing — yoga's PubSub drops messages
76
+ // on topics whose AsyncIterable listener isn't registered yet.
77
+ let ps = LocalGraphQL_SubscriptionResolvers.getPubSub()
78
+ let iter = ps->GraphqlYoga.pubSubSubscribe("onCatalogProduct_stateChanged")
79
+ let consumerPromise = startConsumer(iter, 1000)
80
+ await yieldTick() // ensure listener is registered before publish
81
+
82
+ // Bus payload is the change descriptor matching the AWS StateTopic Lambda
83
+ // output shape: {changeKind, id, sortKeyValue?, seq, state?}.
84
+ let state = JSON.Encode.object(
85
+ Dict.fromArray([
86
+ ("id", JSON.Encode.string("prod-1")),
87
+ ("name", JSON.Encode.string("Widget")),
88
+ ("updatedAt", JSON.Encode.string("2026-05-19T12:00:00Z")),
89
+ ]),
90
+ )
91
+ let descriptor = LocalStateChangeDescriptor.make(
92
+ ~changeKind="Updated",
93
+ ~id="prod-1",
94
+ ~state=Some(state),
95
+ ~seq=LocalStateChangeDescriptor.nextSequence(),
96
+ )
97
+ TestBus.publishStateChange(~name="Product", ~descriptor)
98
+
99
+ let received = await consumerPromise
100
+ switch received->Null.toOption {
101
+ | Some(msg) => expect(msg)->toEqual(descriptor)
102
+ | None => expect("onCatalogProduct_stateChanged: timed out")->toBe("received")
103
+ }
104
+ },
105
+ )
106
+
107
+ testPromise(
108
+ "state change for unregistered ReadModel → no publish",
109
+ async () => {
110
+ module TestBus = LocalBus.Make()
111
+
112
+ LocalGraphQL_SubscriptionResolvers.bridgeSourceB(
113
+ ~subscribeToStateChanges=TestBus.subscribeToStateChanges,
114
+ ~readModelName="Product",
115
+ ~returnTypeName="CatalogProduct",
116
+ )
117
+
118
+ let ps = LocalGraphQL_SubscriptionResolvers.getPubSub()
119
+ let iter = ps->GraphqlYoga.pubSubSubscribe("onCatalogProduct_stateChanged")
120
+ let consumerPromise = startConsumer(iter, 300)
121
+ await yieldTick()
122
+
123
+ // Publish for a DIFFERENT ReadModel — should NOT reach our topic.
124
+ let foreignDescriptor = LocalStateChangeDescriptor.make(
125
+ ~changeKind="Updated",
126
+ ~id="cat-1",
127
+ ~state=Some(JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("cat-1"))]))),
128
+ ~seq=LocalStateChangeDescriptor.nextSequence(),
129
+ )
130
+ TestBus.publishStateChange(~name="Category", ~descriptor=foreignDescriptor)
131
+
132
+ let received = await consumerPromise
133
+ expect(received->Null.toOption)->toEqual(None)
134
+ },
135
+ )
130
136
  })
131
137
 
132
138
  describe("Source A — event-stream bridge", () => {
133
- testPromise("Bus.publishEvent → yoga PubSub subscriber receives event", async () => {
134
- module TestBus = LocalBus.Make()
135
-
136
- LocalGraphQL_SubscriptionResolvers.bridgeSourceA(
137
- ~subscribeToEvents=TestBus.subscribeToEvents,
138
- ~displayName="Catalog",
139
- ~busTopicName="catalog-events",
140
- )
141
-
142
- let ps = LocalGraphQL_SubscriptionResolvers.getPubSub()
143
- let iter = ps->GraphqlYoga.pubSubSubscribe("onCatalogEventLog_eventAppended")
144
- let consumerPromise = startConsumer(iter, 1000)
145
- await yieldTick()
146
-
147
- let event = JSON.Encode.object(
148
- Dict.fromArray([
149
- ("eventType", JSON.Encode.string("ProductAdded")),
150
- ("id", JSON.Encode.string("prod-1")),
151
- ("name", JSON.Encode.string("Widget")),
152
- ]),
153
- )
154
- await TestBus.publishEvent("catalog-events", "svc", testMeta, event)
155
-
156
- let received = await consumerPromise
157
- switch received->Null.toOption {
158
- | Some(msg) => expect(msg)->toEqual(event)
159
- | None => expect("onCatalogEventLog_eventAppended: timed out")->toBe("received")
160
- }
161
- })
139
+ testPromise(
140
+ "Bus.publishEvent yoga PubSub subscriber receives event",
141
+ async () => {
142
+ module TestBus = LocalBus.Make()
143
+
144
+ LocalGraphQL_SubscriptionResolvers.bridgeSourceA(
145
+ ~subscribeToEvents=TestBus.subscribeToEvents,
146
+ ~displayName="Catalog",
147
+ ~busTopicName="catalog-events",
148
+ )
149
+
150
+ let ps = LocalGraphQL_SubscriptionResolvers.getPubSub()
151
+ let iter = ps->GraphqlYoga.pubSubSubscribe("onCatalogEventLog_eventAppended")
152
+ let consumerPromise = startConsumer(iter, 1000)
153
+ await yieldTick()
154
+
155
+ let event = JSON.Encode.object(
156
+ Dict.fromArray([
157
+ ("eventType", JSON.Encode.string("ProductAdded")),
158
+ ("id", JSON.Encode.string("prod-1")),
159
+ ("name", JSON.Encode.string("Widget")),
160
+ ]),
161
+ )
162
+ await TestBus.publishEvent("catalog-events", "svc", testMeta, event)
163
+
164
+ let received = await consumerPromise
165
+ switch received->Null.toOption {
166
+ | Some(msg) => expect(msg)->toEqual(event)
167
+ | None => expect("onCatalogEventLog_eventAppended: timed out")->toBe("received")
168
+ }
169
+ },
170
+ )
162
171
  })
163
172
  })
@@ -31,9 +31,9 @@ let _ = afterAll(() => {
31
31
  // Helper: build a minimal runtime with a completed handler Deferred
32
32
  // ─────────────────────────────────────────────────────────────
33
33
 
34
- let makeRuntime = (
35
- handler: LocalRuntimeEnvironment.handler,
36
- ): ReventlessCore.Runtime.environment<LocalHeartbeatRunner.runtimeParts> => {
34
+ let makeRuntime = (handler: LocalRuntimeEnvironment.handler): ReventlessCore.Runtime.environment<
35
+ LocalHeartbeatRunner.runtimeParts,
36
+ > => {
37
37
  let handlerDeferred: Deferred.t<LocalRuntimeEnvironment.handler, unit> =
38
38
  Deferred.make()->Effect.runSync
39
39
  // Complete before any fiber waits — runSync is safe here
@@ -49,88 +49,106 @@ let makeRuntime = (
49
49
 
50
50
  describe("LocalHeartbeatRunner", () => {
51
51
  describe("make", () => {
52
- testPromise("after advanceTimersByTime(timeout * 60 * 1000), handler is called", async () => {
53
- let count: ref<int> = ref(0)
54
- let runtime = makeRuntime(async (_, _) => {
55
- count := count.contents + 1
56
- })
57
- let _runner = LocalHeartbeatRunner.make(
58
- ~name="hb-test",
59
- ~remoteChannel=Obj.magic(()),
60
- ~timeout=1,
61
- ~runtime,
62
- ~opts={},
63
- )
64
- jest->advanceTimersByTime(1 * 60 * 1000)
65
- // Give the Effect.runPromise microtask one tick to complete
66
- let _ = await Promise.resolve()
67
- expect(count.contents)->toBe(1)
68
- LocalHeartbeatRunner.reset()
69
- })
52
+ testPromise(
53
+ "after advanceTimersByTime(timeout * 60 * 1000), handler is called",
54
+ async () => {
55
+ let count: ref<int> = ref(0)
56
+ let runtime = makeRuntime(
57
+ async (_, _) => {
58
+ count := count.contents + 1
59
+ },
60
+ )
61
+ let _runner = LocalHeartbeatRunner.make(
62
+ ~name="hb-test",
63
+ ~remoteChannel=Obj.magic(),
64
+ ~timeout=1,
65
+ ~runtime,
66
+ ~opts={},
67
+ )
68
+ jest->advanceTimersByTime(1 * 60 * 1000)
69
+ // Give the Effect.runPromise microtask one tick to complete
70
+ let _ = await Promise.resolve()
71
+ expect(count.contents)->toBe(1)
72
+ LocalHeartbeatRunner.reset()
73
+ },
74
+ )
70
75
 
71
- testPromise("handler fires again on each subsequent interval", async () => {
72
- let count: ref<int> = ref(0)
73
- let runtime = makeRuntime(async (_, _) => {
74
- count := count.contents + 1
75
- })
76
- let _runner = LocalHeartbeatRunner.make(
77
- ~name="hb-test-2",
78
- ~remoteChannel=Obj.magic(()),
79
- ~timeout=2,
80
- ~runtime,
81
- ~opts={},
82
- )
83
- jest->advanceTimersByTime(2 * 60 * 1000)
84
- let _ = await Promise.resolve()
85
- jest->advanceTimersByTime(2 * 60 * 1000)
86
- let _ = await Promise.resolve()
87
- expect(count.contents)->toBe(2)
88
- LocalHeartbeatRunner.reset()
89
- })
76
+ testPromise(
77
+ "handler fires again on each subsequent interval",
78
+ async () => {
79
+ let count: ref<int> = ref(0)
80
+ let runtime = makeRuntime(
81
+ async (_, _) => {
82
+ count := count.contents + 1
83
+ },
84
+ )
85
+ let _runner = LocalHeartbeatRunner.make(
86
+ ~name="hb-test-2",
87
+ ~remoteChannel=Obj.magic(),
88
+ ~timeout=2,
89
+ ~runtime,
90
+ ~opts={},
91
+ )
92
+ jest->advanceTimersByTime(2 * 60 * 1000)
93
+ let _ = await Promise.resolve()
94
+ jest->advanceTimersByTime(2 * 60 * 1000)
95
+ let _ = await Promise.resolve()
96
+ expect(count.contents)->toBe(2)
97
+ LocalHeartbeatRunner.reset()
98
+ },
99
+ )
90
100
 
91
- testPromise("unresolved Deferred does not crash when interval fires", async () => {
92
- // Deferred intentionally left incomplete simulates handler not yet registered.
93
- // Effect.runPromise stays pending (no crash); test verifies no exception.
94
- let handlerDeferred: Deferred.t<LocalRuntimeEnvironment.handler, unit> =
95
- Deferred.make()->Effect.runSync
96
- let runtime: ReventlessCore.Runtime.environment<LocalHeartbeatRunner.runtimeParts> = {
97
- parts: {
98
- handlerDeferred,
99
- subscriptionLatch: Effect.makeLatch(false)->Effect.runSync,
100
- },
101
- resources: [],
102
- }
103
- let _runner = LocalHeartbeatRunner.make(
104
- ~name="hb-no-handler",
105
- ~remoteChannel=Obj.magic(()),
106
- ~timeout=1,
107
- ~runtime,
108
- ~opts={},
109
- )
110
- jest->advanceTimersByTime(1 * 60 * 1000)
111
- expect(true)->toBe(true)
112
- LocalHeartbeatRunner.reset()
113
- // Complete the Deferred to avoid open handle — interval is already cleared
114
- Deferred.succeed(handlerDeferred, async (_, _) => ())->Effect.runSync->ignore
115
- })
101
+ testPromise(
102
+ "unresolved Deferred does not crash when interval fires",
103
+ async () => {
104
+ // Deferred intentionally left incomplete — simulates handler not yet registered.
105
+ // Effect.runPromise stays pending (no crash); test verifies no exception.
106
+ let handlerDeferred: Deferred.t<LocalRuntimeEnvironment.handler, unit> =
107
+ Deferred.make()->Effect.runSync
108
+ let runtime: ReventlessCore.Runtime.environment<LocalHeartbeatRunner.runtimeParts> = {
109
+ parts: {
110
+ handlerDeferred,
111
+ subscriptionLatch: Effect.makeLatch(false)->Effect.runSync,
112
+ },
113
+ resources: [],
114
+ }
115
+ let _runner = LocalHeartbeatRunner.make(
116
+ ~name="hb-no-handler",
117
+ ~remoteChannel=Obj.magic(),
118
+ ~timeout=1,
119
+ ~runtime,
120
+ ~opts={},
121
+ )
122
+ jest->advanceTimersByTime(1 * 60 * 1000)
123
+ expect(true)->toBe(true)
124
+ LocalHeartbeatRunner.reset()
125
+ // Complete the Deferred to avoid open handle — interval is already cleared
126
+ Deferred.succeed(handlerDeferred, async (_, _) => ())->Effect.runSync->ignore
127
+ },
128
+ )
116
129
  })
117
130
 
118
131
  describe("reset", () => {
119
- testPromise("clears all intervals; subsequent advance does not fire handler", async () => {
120
- let count: ref<int> = ref(0)
121
- let runtime = makeRuntime(async (_, _) => {
122
- count := count.contents + 1
123
- })
124
- let _runner = LocalHeartbeatRunner.make(
125
- ~name="hb-reset",
126
- ~remoteChannel=Obj.magic(()),
127
- ~timeout=1,
128
- ~runtime,
129
- ~opts={},
130
- )
131
- LocalHeartbeatRunner.reset()
132
- jest->advanceTimersByTime(1 * 60 * 1000)
133
- expect(count.contents)->toBe(0)
134
- })
132
+ testPromise(
133
+ "clears all intervals; subsequent advance does not fire handler",
134
+ async () => {
135
+ let count: ref<int> = ref(0)
136
+ let runtime = makeRuntime(
137
+ async (_, _) => {
138
+ count := count.contents + 1
139
+ },
140
+ )
141
+ let _runner = LocalHeartbeatRunner.make(
142
+ ~name="hb-reset",
143
+ ~remoteChannel=Obj.magic(),
144
+ ~timeout=1,
145
+ ~runtime,
146
+ ~opts={},
147
+ )
148
+ LocalHeartbeatRunner.reset()
149
+ jest->advanceTimersByTime(1 * 60 * 1000)
150
+ expect(count.contents)->toBe(0)
151
+ },
152
+ )
135
153
  })
136
154
  })
@@ -68,7 +68,10 @@ let payload = (result: GraphqlYoga.executionResult): JSON.t =>
68
68
  ->Option.getOr(JSON.Encode.null)
69
69
 
70
70
  let str = (node: JSON.t, key: string): option<string> =>
71
- node->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
71
+ node
72
+ ->JSON.Decode.object
73
+ ->Option.flatMap(d => d->Dict.get(key))
74
+ ->Option.flatMap(JSON.Decode.string)
72
75
 
73
76
  let num = (node: JSON.t, key: string): option<float> =>
74
77
  node->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.float)
@@ -40,33 +40,34 @@ type httpRes
40
40
  let postJson = (path: string, body: dict<JSON.t>): promise<(int, JSON.t)> =>
41
41
  Promise.make((resolve, reject) => {
42
42
  let bodyStr = body->JSON.Encode.object->JSON.stringify
43
- let req =
44
- _request(
45
- {
46
- hostname: "localhost",
47
- port,
48
- path,
49
- method: "POST",
50
- headers: Dict.fromArray([
51
- ("content-type", "application/json"),
52
- ("content-length", bodyStr->String.length->Int.toString),
53
- ]),
54
- },
55
- res => {
56
- let status = _resStatusCode(res)
57
- let buf = ref("")
58
- res->_resSetEncoding("utf8")
59
- res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
60
- res->_resOnEnd(() => {
43
+ let req = _request(
44
+ {
45
+ hostname: "localhost",
46
+ port,
47
+ path,
48
+ method: "POST",
49
+ headers: Dict.fromArray([
50
+ ("content-type", "application/json"),
51
+ ("content-length", bodyStr->String.length->Int.toString),
52
+ ]),
53
+ },
54
+ res => {
55
+ let status = _resStatusCode(res)
56
+ let buf = ref("")
57
+ res->_resSetEncoding("utf8")
58
+ res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
59
+ res->_resOnEnd(
60
+ () => {
61
61
  let parsed = try {
62
62
  buf.contents->JSON.parseOrThrow
63
63
  } catch {
64
64
  | _ => JSON.Encode.null
65
65
  }
66
66
  resolve((status, parsed))
67
- })
68
- },
69
- )
67
+ },
68
+ )
69
+ },
70
+ )
70
71
  req->_reqOnError(err => reject(Obj.magic(err)))
71
72
  req->_reqWrite(bodyStr)
72
73
  req->_reqEnd
@@ -74,40 +75,38 @@ let postJson = (path: string, body: dict<JSON.t>): promise<(int, JSON.t)> =>
74
75
 
75
76
  let postEmpty = (path: string): promise<int> =>
76
77
  Promise.make((resolve, reject) => {
77
- let req =
78
- _request(
79
- {hostname: "localhost", port, path, method: "POST", headers: Dict.make()},
80
- res => {
81
- let status = _resStatusCode(res)
82
- // Drain (otherwise the socket may stay open under HTTP keep-alive).
83
- res->_resOnData(_ => ())
84
- res->_resOnEnd(() => resolve(status))
85
- },
86
- )
78
+ let req = _request(
79
+ {hostname: "localhost", port, path, method: "POST", headers: Dict.make()},
80
+ res => {
81
+ let status = _resStatusCode(res)
82
+ // Drain (otherwise the socket may stay open under HTTP keep-alive).
83
+ res->_resOnData(_ => ())
84
+ res->_resOnEnd(() => resolve(status))
85
+ },
86
+ )
87
87
  req->_reqOnError(err => reject(Obj.magic(err)))
88
88
  req->_reqEnd
89
89
  })
90
90
 
91
91
  let postRaw = (path: string, body: string): promise<int> =>
92
92
  Promise.make((resolve, reject) => {
93
- let req =
94
- _request(
95
- {
96
- hostname: "localhost",
97
- port,
98
- path,
99
- method: "POST",
100
- headers: Dict.fromArray([
101
- ("content-type", "application/json"),
102
- ("content-length", body->String.length->Int.toString),
103
- ]),
104
- },
105
- res => {
106
- let status = _resStatusCode(res)
107
- res->_resOnData(_ => ())
108
- res->_resOnEnd(() => resolve(status))
109
- },
110
- )
93
+ let req = _request(
94
+ {
95
+ hostname: "localhost",
96
+ port,
97
+ path,
98
+ method: "POST",
99
+ headers: Dict.fromArray([
100
+ ("content-type", "application/json"),
101
+ ("content-length", body->String.length->Int.toString),
102
+ ]),
103
+ },
104
+ res => {
105
+ let status = _resStatusCode(res)
106
+ res->_resOnData(_ => ())
107
+ res->_resOnEnd(() => resolve(status))
108
+ },
109
+ )
111
110
  req->_reqOnError(err => reject(Obj.magic(err)))
112
111
  req->_reqWrite(body)
113
112
  req->_reqEnd
@@ -148,14 +147,13 @@ afterAll(() => {
148
147
  // ── Tests ──────────────────────────────────────────────────────────────────
149
148
 
150
149
  testPromise("POST /__inmemory/login returns {token, identity} for valid credentials", async () => {
151
- let (status, body) =
152
- await postJson(
153
- "/__inmemory/login",
154
- Dict.fromArray([
155
- ("username", JSON.Encode.string("alice")),
156
- ("password", JSON.Encode.string("alice-pw")),
157
- ]),
158
- )
150
+ let (status, body) = await postJson(
151
+ "/__inmemory/login",
152
+ Dict.fromArray([
153
+ ("username", JSON.Encode.string("alice")),
154
+ ("password", JSON.Encode.string("alice-pw")),
155
+ ]),
156
+ )
159
157
  expect(status)->toEqual(200)
160
158
  let token = getString(body, "token")
161
159
  expect(token->String.length > 0)->toEqual(true)
@@ -171,27 +169,25 @@ testPromise("POST /__inmemory/login returns {token, identity} for valid credenti
171
169
  })
172
170
 
173
171
  testPromise("POST /__inmemory/login returns 401 for wrong password", async () => {
174
- let (status, body) =
175
- await postJson(
176
- "/__inmemory/login",
177
- Dict.fromArray([
178
- ("username", JSON.Encode.string("alice")),
179
- ("password", JSON.Encode.string("WRONG")),
180
- ]),
181
- )
172
+ let (status, body) = await postJson(
173
+ "/__inmemory/login",
174
+ Dict.fromArray([
175
+ ("username", JSON.Encode.string("alice")),
176
+ ("password", JSON.Encode.string("WRONG")),
177
+ ]),
178
+ )
182
179
  expect(status)->toEqual(401)
183
180
  expect(getString(body, "error")->String.length > 0)->toEqual(true)
184
181
  })
185
182
 
186
183
  testPromise("POST /__inmemory/login returns 401 for missing user", async () => {
187
- let (status, _) =
188
- await postJson(
189
- "/__inmemory/login",
190
- Dict.fromArray([
191
- ("username", JSON.Encode.string("ghost")),
192
- ("password", JSON.Encode.string("x")),
193
- ]),
194
- )
184
+ let (status, _) = await postJson(
185
+ "/__inmemory/login",
186
+ Dict.fromArray([
187
+ ("username", JSON.Encode.string("ghost")),
188
+ ("password", JSON.Encode.string("x")),
189
+ ]),
190
+ )
195
191
  expect(status)->toEqual(401)
196
192
  })
197
193
 
@@ -206,19 +202,17 @@ testPromise("POST /__inmemory/logout always returns 204", async () => {
206
202
  })
207
203
 
208
204
  testPromise("Bearer token issued via HTTP round-trips through authenticate", async () => {
209
- let (_, body) =
210
- await postJson(
211
- "/__inmemory/login",
212
- Dict.fromArray([
213
- ("username", JSON.Encode.string("alice")),
214
- ("password", JSON.Encode.string("alice-pw")),
215
- ]),
216
- )
205
+ let (_, body) = await postJson(
206
+ "/__inmemory/login",
207
+ Dict.fromArray([
208
+ ("username", JSON.Encode.string("alice")),
209
+ ("password", JSON.Encode.string("alice-pw")),
210
+ ]),
211
+ )
217
212
  let token = getString(body, "token")
218
- let result =
219
- await LocalAuth.authenticate({
220
- headers: Dict.fromArray([("authorization", "Bearer " ++ token)]),
221
- })
213
+ let result = await LocalAuth.authenticate({
214
+ headers: Dict.fromArray([("authorization", "Bearer " ++ token)]),
215
+ })
222
216
  switch result {
223
217
  | Authenticated(identity) =>
224
218
  expect(identity.username)->toEqual("alice")