@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.
- package/CHANGELOG.md +30 -0
- package/package.json +13 -13
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +12 -16
- package/src/adapter/Auth/LocalAuth.res.mjs +2 -1
- package/src/adapter/Auth/UserStore.res +13 -40
- package/src/adapter/Auth/UserStore.res.mjs +11 -67
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res +6 -0
- package/src/adapter/LocalCapabilities.res.mjs +5 -2
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +10 -6
- package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -1
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- 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(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
count
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
count
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
count
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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")
|