@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
|
@@ -18,9 +18,9 @@ let testMeta: Reventless.Message.meta = {
|
|
|
18
18
|
// Helper: build a minimal runtime with a completed handler Deferred
|
|
19
19
|
// ─────────────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
|
-
let makeRuntime = (
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
let makeRuntime = (handler: LocalRuntimeEnvironment.handler): ReventlessCore.Runtime.environment<
|
|
22
|
+
LocalRuntimeEnvironment.parts,
|
|
23
|
+
> => {
|
|
24
24
|
let handlerDeferred: Deferred.t<LocalRuntimeEnvironment.handler, unit> =
|
|
25
25
|
Deferred.make()->Effect.runSync
|
|
26
26
|
Deferred.succeed(handlerDeferred, handler)->Effect.runSync->ignore
|
|
@@ -35,132 +35,161 @@ let makeRuntime = (
|
|
|
35
35
|
|
|
36
36
|
describe("LocalEventCollectorChannel", () => {
|
|
37
37
|
describe("make", () => {
|
|
38
|
-
testPromise(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
("
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
testPromise(
|
|
39
|
+
"collects all event topic resources as channel resources",
|
|
40
|
+
async () => {
|
|
41
|
+
module TestBus = LocalBus.Make()
|
|
42
|
+
module TestPublisher = LocalEventTopicPublisher.Make(TestBus)
|
|
43
|
+
module TestCollector = LocalEventCollectorChannel.Make(TestBus)
|
|
44
|
+
let pubA = TestPublisher.make(~name="topicA", ~storageResources=[], ~owner=None, ~opts={})
|
|
45
|
+
let pubB = TestPublisher.make(~name="topicB", ~storageResources=[], ~owner=None, ~opts={})
|
|
46
|
+
let eventTopics = Dict.fromArray([
|
|
47
|
+
("topicA", {ReventlessInfra.EventTopic.resources: pubA.resources}),
|
|
48
|
+
("topicB", {ReventlessInfra.EventTopic.resources: pubB.resources}),
|
|
49
|
+
])
|
|
50
|
+
let ch = TestCollector.make(~name="collector", ~eventTopics, ~owner=None, ~opts={})
|
|
51
|
+
// Each publisher returns 1 resource, so 2 total
|
|
52
|
+
expect(ch.resources->Array.length)->toBe(2)
|
|
53
|
+
},
|
|
54
|
+
)
|
|
52
55
|
|
|
53
|
-
testPromise(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
testPromise(
|
|
57
|
+
"empty eventTopics produces empty resources",
|
|
58
|
+
async () => {
|
|
59
|
+
module TestBus = LocalBus.Make()
|
|
60
|
+
module TestCollector = LocalEventCollectorChannel.Make(TestBus)
|
|
61
|
+
let ch = TestCollector.make(
|
|
62
|
+
~name="collector",
|
|
63
|
+
~eventTopics=Dict.make(),
|
|
64
|
+
~owner=None,
|
|
65
|
+
~opts={},
|
|
66
|
+
)
|
|
67
|
+
expect(ch.resources->Array.length)->toBe(0)
|
|
68
|
+
},
|
|
69
|
+
)
|
|
59
70
|
})
|
|
60
71
|
|
|
61
72
|
describe("connect", () => {
|
|
62
|
-
testPromise(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
received
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
testPromise(
|
|
74
|
+
"subscribes to event topic; published events reach the handler",
|
|
75
|
+
async () => {
|
|
76
|
+
module TestBus = LocalBus.Make()
|
|
77
|
+
module TestPublisher = LocalEventTopicPublisher.Make(TestBus)
|
|
78
|
+
module TestCollector = LocalEventCollectorChannel.Make(TestBus)
|
|
79
|
+
let received: ref<int> = ref(0)
|
|
80
|
+
let runtime = makeRuntime(
|
|
81
|
+
async (_, _ctx) => {
|
|
82
|
+
received := received.contents + 1
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
let pub = TestPublisher.make(~name="eventA", ~storageResources=[], ~owner=None, ~opts={})
|
|
86
|
+
let eventTopics = Dict.fromArray([
|
|
87
|
+
("eventA", {ReventlessInfra.EventTopic.resources: pub.resources}),
|
|
88
|
+
])
|
|
89
|
+
let ch = TestCollector.make(~name="collector", ~eventTopics, ~owner=None, ~opts={})
|
|
90
|
+
let channelSpec: ReventlessCore.EventCollector_Adapter.channelSpec<JSON.t, unit, unit> = {
|
|
91
|
+
channel: ch,
|
|
92
|
+
eventTopics,
|
|
93
|
+
resources: [],
|
|
94
|
+
}
|
|
95
|
+
let _ = TestCollector.connect(
|
|
96
|
+
~name="collector",
|
|
97
|
+
~channelSpecs=[channelSpec],
|
|
98
|
+
~runtime,
|
|
99
|
+
~opts={},
|
|
100
|
+
)
|
|
101
|
+
// Await the subscriptionLatch — opens after Output.apply fires and subscription registers
|
|
102
|
+
await runtime.parts.subscriptionLatch->Latch.await_->Effect.runPromise
|
|
103
|
+
let publishFn = await pub.publishJson->TestRunner.resolve
|
|
104
|
+
await publishFn("svc", testMeta, JSON.Null)
|
|
105
|
+
expect(received.contents)->toBe(1)
|
|
106
|
+
},
|
|
107
|
+
)
|
|
92
108
|
|
|
93
|
-
testPromise(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
received
|
|
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
|
-
|
|
109
|
+
testPromise(
|
|
110
|
+
"multiple event topics all deliver to the same handler",
|
|
111
|
+
async () => {
|
|
112
|
+
module TestBus = LocalBus.Make()
|
|
113
|
+
module TestPublisher = LocalEventTopicPublisher.Make(TestBus)
|
|
114
|
+
module TestCollector = LocalEventCollectorChannel.Make(TestBus)
|
|
115
|
+
let received: ref<int> = ref(0)
|
|
116
|
+
let runtime = makeRuntime(
|
|
117
|
+
async (_, _) => {
|
|
118
|
+
received := received.contents + 1
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
let pub1 = TestPublisher.make(~name="topicX", ~storageResources=[], ~owner=None, ~opts={})
|
|
122
|
+
let pub2 = TestPublisher.make(~name="topicY", ~storageResources=[], ~owner=None, ~opts={})
|
|
123
|
+
let eventTopics = Dict.fromArray([
|
|
124
|
+
("topicX", {ReventlessInfra.EventTopic.resources: pub1.resources}),
|
|
125
|
+
("topicY", {ReventlessInfra.EventTopic.resources: pub2.resources}),
|
|
126
|
+
])
|
|
127
|
+
let ch = TestCollector.make(~name="collector", ~eventTopics, ~owner=None, ~opts={})
|
|
128
|
+
let channelSpec: ReventlessCore.EventCollector_Adapter.channelSpec<JSON.t, unit, unit> = {
|
|
129
|
+
channel: ch,
|
|
130
|
+
eventTopics,
|
|
131
|
+
resources: [],
|
|
132
|
+
}
|
|
133
|
+
let _ = TestCollector.connect(
|
|
134
|
+
~name="collector",
|
|
135
|
+
~channelSpecs=[channelSpec],
|
|
136
|
+
~runtime,
|
|
137
|
+
~opts={},
|
|
138
|
+
)
|
|
139
|
+
// Wait for both subscriptions via the publish functions (each Output resolves per-topic)
|
|
140
|
+
let pub1Fn = await pub1.publishJson->TestRunner.resolve
|
|
141
|
+
let pub2Fn = await pub2.publishJson->TestRunner.resolve
|
|
142
|
+
await pub1Fn("svc", testMeta, JSON.Null)
|
|
143
|
+
await pub2Fn("svc", testMeta, JSON.Null)
|
|
144
|
+
expect(received.contents)->toBe(2)
|
|
145
|
+
},
|
|
146
|
+
)
|
|
126
147
|
|
|
127
|
-
testPromise(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Deferred
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
~name="collector",
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
148
|
+
testPromise(
|
|
149
|
+
"unresolved Deferred does not crash on event delivery",
|
|
150
|
+
async () => {
|
|
151
|
+
module TestBus = LocalBus.Make()
|
|
152
|
+
module TestPublisher = LocalEventTopicPublisher.Make(TestBus)
|
|
153
|
+
module TestCollector = LocalEventCollectorChannel.Make(TestBus)
|
|
154
|
+
// Deferred left incomplete — bus callback starts a pending Effect (no crash)
|
|
155
|
+
let handlerDeferred: Deferred.t<LocalRuntimeEnvironment.handler, unit> =
|
|
156
|
+
Deferred.make()->Effect.runSync
|
|
157
|
+
let runtime: ReventlessCore.Runtime.environment<LocalRuntimeEnvironment.parts> = {
|
|
158
|
+
parts: {
|
|
159
|
+
handlerDeferred,
|
|
160
|
+
subscriptionLatch: Effect.makeLatch(false)->Effect.runSync,
|
|
161
|
+
},
|
|
162
|
+
resources: [],
|
|
163
|
+
}
|
|
164
|
+
let pub = TestPublisher.make(
|
|
165
|
+
~name="noHandlerTopic",
|
|
166
|
+
~storageResources=[],
|
|
167
|
+
~owner=None,
|
|
168
|
+
~opts={},
|
|
169
|
+
)
|
|
170
|
+
let eventTopics = Dict.fromArray([
|
|
171
|
+
("noHandlerTopic", {ReventlessInfra.EventTopic.resources: pub.resources}),
|
|
172
|
+
])
|
|
173
|
+
let ch = TestCollector.make(~name="collector", ~eventTopics, ~owner=None, ~opts={})
|
|
174
|
+
let channelSpec: ReventlessCore.EventCollector_Adapter.channelSpec<JSON.t, unit, unit> = {
|
|
175
|
+
channel: ch,
|
|
176
|
+
eventTopics,
|
|
177
|
+
resources: [],
|
|
178
|
+
}
|
|
179
|
+
let _ = TestCollector.connect(
|
|
180
|
+
~name="collector",
|
|
181
|
+
~channelSpecs=[channelSpec],
|
|
182
|
+
~runtime,
|
|
183
|
+
~opts={},
|
|
184
|
+
)
|
|
185
|
+
await runtime.parts.subscriptionLatch->Latch.await_->Effect.runPromise
|
|
186
|
+
let publishFn = await pub.publishJson->TestRunner.resolve
|
|
187
|
+
// Fire-and-forget — the Deferred.await_ will stay pending
|
|
188
|
+
publishFn("svc", testMeta, JSON.Null)->ignore
|
|
189
|
+
expect(true)->toBe(true)
|
|
190
|
+
// Complete the Deferred to avoid open handle
|
|
191
|
+
Deferred.succeed(handlerDeferred, async (_, _) => ())->Effect.runSync->ignore
|
|
192
|
+
},
|
|
193
|
+
)
|
|
165
194
|
})
|
|
166
195
|
})
|
|
@@ -18,7 +18,14 @@ let metaOf = (~user=?, ~time="2026-01-01T00:00:00Z", ()) =>
|
|
|
18
18
|
("causationId", JSON.Encode.null),
|
|
19
19
|
])->JSON.Encode.object
|
|
20
20
|
|
|
21
|
-
let rec_ = (
|
|
21
|
+
let rec_ = (
|
|
22
|
+
~position,
|
|
23
|
+
~eventType="OrderPlaced",
|
|
24
|
+
~tags=[],
|
|
25
|
+
~user=?,
|
|
26
|
+
~time="2026-01-01T00:00:00Z",
|
|
27
|
+
(),
|
|
28
|
+
): EH.record => {
|
|
22
29
|
position,
|
|
23
30
|
eventType,
|
|
24
31
|
payload: JSON.Encode.object(Dict.make()),
|
|
@@ -86,9 +93,8 @@ describe("EventHistoryResolvers_GraphQL — position ordering", () => {
|
|
|
86
93
|
})
|
|
87
94
|
|
|
88
95
|
testSync("a cursor bound crossing a digit boundary skips nothing", () => {
|
|
89
|
-
let records =
|
|
90
|
-
rec_(~position=i->Int.toString, ())
|
|
91
|
-
)
|
|
96
|
+
let records =
|
|
97
|
+
Array.make(~length=12, 0)->Array.mapWithIndex((_, i) => rec_(~position=i->Int.toString, ()))
|
|
92
98
|
// Page 1 ends at position 9; page 2 must start at 10, not at 1.
|
|
93
99
|
let page1 = EH.paginate(~records, ~args=args(~first=10, ()))
|
|
94
100
|
expect(page1->positionsOf->Array.length)->toBe(10)
|
|
@@ -101,9 +107,30 @@ describe("EventHistoryResolvers_GraphQL — position ordering", () => {
|
|
|
101
107
|
|
|
102
108
|
describe("EventHistoryResolvers_GraphQL — filtering", () => {
|
|
103
109
|
let records = [
|
|
104
|
-
rec_(
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
rec_(
|
|
111
|
+
~position="1",
|
|
112
|
+
~eventType="OrderPlaced",
|
|
113
|
+
~tags=[tag("orderId", "o-1")],
|
|
114
|
+
~user="alice",
|
|
115
|
+
~time="2026-01-01T00:00:00Z",
|
|
116
|
+
(),
|
|
117
|
+
),
|
|
118
|
+
rec_(
|
|
119
|
+
~position="2",
|
|
120
|
+
~eventType="OrderShipped",
|
|
121
|
+
~tags=[tag("orderId", "o-1")],
|
|
122
|
+
~user="bob",
|
|
123
|
+
~time="2026-01-05T00:00:00Z",
|
|
124
|
+
(),
|
|
125
|
+
),
|
|
126
|
+
rec_(
|
|
127
|
+
~position="3",
|
|
128
|
+
~eventType="OrderPlaced",
|
|
129
|
+
~tags=[tag("orderId", "o-2")],
|
|
130
|
+
~user="alice",
|
|
131
|
+
~time="2026-01-09T00:00:00Z",
|
|
132
|
+
(),
|
|
133
|
+
),
|
|
107
134
|
]
|
|
108
135
|
let filtered = f => records->Array.filter(r => EH.matchesFilter(r, f))->Array.map(r => r.position)
|
|
109
136
|
|
|
@@ -129,9 +156,9 @@ describe("EventHistoryResolvers_GraphQL — filtering", () => {
|
|
|
129
156
|
testSync("time range is inclusive on both bounds", () => {
|
|
130
157
|
expect(filtered({timeFrom: "2026-01-05T00:00:00Z"}))->toEqual(["2", "3"])
|
|
131
158
|
expect(filtered({timeTo: "2026-01-05T00:00:00Z"}))->toEqual(["1", "2"])
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
expect(filtered({timeFrom: "2026-01-05T00:00:00Z", timeTo: "2026-01-05T00:00:00Z"}))->toEqual([
|
|
160
|
+
"2",
|
|
161
|
+
])
|
|
135
162
|
})
|
|
136
163
|
|
|
137
164
|
testSync("an empty filter keeps everything", () => {
|
|
@@ -154,10 +181,7 @@ describe("EventHistoryResolvers_GraphQL — argument decoding", () => {
|
|
|
154
181
|
("entityId", JSON.Encode.string("o-1")),
|
|
155
182
|
("tagKey", JSON.Encode.string("orderId")),
|
|
156
183
|
("tagValue", JSON.Encode.string("o-1")),
|
|
157
|
-
(
|
|
158
|
-
"eventTypes",
|
|
159
|
-
[JSON.Encode.string("OrderPlaced")]->JSON.Encode.array,
|
|
160
|
-
),
|
|
184
|
+
("eventTypes", [JSON.Encode.string("OrderPlaced")]->JSON.Encode.array),
|
|
161
185
|
("user", JSON.Encode.string("alice")),
|
|
162
186
|
("timeFrom", JSON.Encode.string("2026-01-01")),
|
|
163
187
|
("timeTo", JSON.Encode.string("2026-02-01")),
|
|
@@ -183,9 +207,10 @@ describe("EventHistoryResolvers_GraphQL — argument decoding", () => {
|
|
|
183
207
|
})
|
|
184
208
|
|
|
185
209
|
describe("EventHistoryResolvers_GraphQL — pagination", () => {
|
|
186
|
-
let records =
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
let records =
|
|
211
|
+
Array.make(~length=5, 0)->Array.mapWithIndex((_, i) =>
|
|
212
|
+
rec_(~position=(i + 1)->Int.toString, ())
|
|
213
|
+
)
|
|
189
214
|
|
|
190
215
|
testSync("forward paging reports hasNextPage until exhausted", () => {
|
|
191
216
|
let page = EH.paginate(~records, ~args=args(~first=2, ()))
|
|
@@ -242,7 +267,10 @@ describe("EventHistoryResolvers_GraphQL — naming the caller", () => {
|
|
|
242
267
|
|
|
243
268
|
testSync("say so when the caller sent no filter at all", () =>
|
|
244
269
|
expect(
|
|
245
|
-
EH.describeCaller(
|
|
270
|
+
EH.describeCaller(
|
|
271
|
+
~ctx=ctxOf(~operationName="AuditTrail", ~userId="local-admin", ()),
|
|
272
|
+
~filter={},
|
|
273
|
+
),
|
|
246
274
|
)->toBe(`operation "AuditTrail" as local-admin, with no filter`)
|
|
247
275
|
)
|
|
248
276
|
|
|
@@ -28,54 +28,66 @@ let backends = [("memory", makeMemoryOps), ("sqlite", makeSqliteOps)]
|
|
|
28
28
|
|
|
29
29
|
describe("EventLog snapshot backend parity", () => {
|
|
30
30
|
backends->Array.forEach(((label, makeOps)) => {
|
|
31
|
-
testPromise(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
testPromise(
|
|
32
|
+
`${label}: latestSnapshot is None before any write`,
|
|
33
|
+
async () => {
|
|
34
|
+
let ops = await makeOps()
|
|
35
|
+
expect(await ops.latestSnapshot("id-1"))->toEqual(Ok(None))
|
|
36
|
+
},
|
|
37
|
+
)
|
|
35
38
|
|
|
36
|
-
testPromise(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
testPromise(
|
|
40
|
+
`${label}: snapshot round-trips and keep-one overwrites`,
|
|
41
|
+
async () => {
|
|
42
|
+
let ops = await makeOps()
|
|
43
|
+
let state = JSON.Encode.object(Dict.fromArray([("n", JSON.Encode.int(1))]))
|
|
44
|
+
expect(
|
|
45
|
+
await ops.writeSnapshot(
|
|
46
|
+
"id-1",
|
|
47
|
+
{ReventlessCore.EventLog.seqNr: 2, state, schemaHash: "h1"},
|
|
48
|
+
),
|
|
49
|
+
)->toEqual(Ok())
|
|
50
|
+
expect(await ops.latestSnapshot("id-1"))->toEqual(
|
|
51
|
+
Ok(Some({ReventlessCore.EventLog.seqNr: 2, state, schemaHash: "h1"})),
|
|
52
|
+
)
|
|
53
|
+
let state2 = JSON.Encode.object(Dict.fromArray([("n", JSON.Encode.int(9))]))
|
|
54
|
+
let _ = await ops.writeSnapshot(
|
|
41
55
|
"id-1",
|
|
42
|
-
{ReventlessCore.EventLog.seqNr:
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let _ = await ops.writeSnapshot(
|
|
50
|
-
"id-1",
|
|
51
|
-
{ReventlessCore.EventLog.seqNr: 9, state: state2, schemaHash: "h2"},
|
|
52
|
-
)
|
|
53
|
-
expect(await ops.latestSnapshot("id-1"))->toEqual(
|
|
54
|
-
Ok(Some({ReventlessCore.EventLog.seqNr: 9, state: state2, schemaHash: "h2"})),
|
|
55
|
-
)
|
|
56
|
-
})
|
|
56
|
+
{ReventlessCore.EventLog.seqNr: 9, state: state2, schemaHash: "h2"},
|
|
57
|
+
)
|
|
58
|
+
expect(await ops.latestSnapshot("id-1"))->toEqual(
|
|
59
|
+
Ok(Some({ReventlessCore.EventLog.seqNr: 9, state: state2, schemaHash: "h2"})),
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
)
|
|
57
63
|
|
|
58
|
-
testPromise(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
testPromise(
|
|
65
|
+
`${label}: snapshots are per-aggregate-id`,
|
|
66
|
+
async () => {
|
|
67
|
+
let ops = await makeOps()
|
|
68
|
+
let state = JSON.Encode.object(Dict.fromArray([("n", JSON.Encode.int(1))]))
|
|
69
|
+
let _ = await ops.writeSnapshot(
|
|
70
|
+
"id-a",
|
|
71
|
+
{ReventlessCore.EventLog.seqNr: 1, state, schemaHash: "h"},
|
|
72
|
+
)
|
|
73
|
+
expect(await ops.latestSnapshot("id-b"))->toEqual(Ok(None))
|
|
74
|
+
},
|
|
75
|
+
)
|
|
67
76
|
|
|
68
|
-
testPromise(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
testPromise(
|
|
78
|
+
`${label}: replayStream ~fromSeq equals the tail of a full replay`,
|
|
79
|
+
async () => {
|
|
80
|
+
let ops = await makeOps()
|
|
81
|
+
let ev = i => JSON.Encode.object(Dict.fromArray([("i", JSON.Encode.int(i))]))
|
|
82
|
+
let _ = await ops.append(0, "id-1", [ev(0), ev(1), ev(2), ev(3), ev(4)])
|
|
83
|
+
let delta = await ops.replayStream("id-1", ~fromSeq=3)->Stream.runCollect->Effect.runPromise
|
|
84
|
+
expect(delta)->toEqual([ev(3), ev(4)])
|
|
85
|
+
// fromSeq at the head yields nothing; omitted fromSeq yields everything.
|
|
86
|
+
let empty = await ops.replayStream("id-1", ~fromSeq=5)->Stream.runCollect->Effect.runPromise
|
|
87
|
+
expect(empty)->toEqual([])
|
|
88
|
+
let full = await ops.replayStream("id-1")->Stream.runCollect->Effect.runPromise
|
|
89
|
+
expect(full->Array.length)->toBe(5)
|
|
90
|
+
},
|
|
91
|
+
)
|
|
80
92
|
})
|
|
81
93
|
})
|
|
@@ -19,12 +19,8 @@ describe("EventLogStorage_Sqlite", () => {
|
|
|
19
19
|
let s = Storage.make(~name="agg", ~owner=None, ~opts)
|
|
20
20
|
let ops = await s.operations->TestRunner.resolve
|
|
21
21
|
|
|
22
|
-
let event1 = JSON.Encode.object(
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
let event2 = JSON.Encode.object(
|
|
26
|
-
Dict.fromArray([("tag", JSON.Encode.string("Renamed"))]),
|
|
27
|
-
)
|
|
22
|
+
let event1 = JSON.Encode.object(Dict.fromArray([("tag", JSON.Encode.string("Created"))]))
|
|
23
|
+
let event2 = JSON.Encode.object(Dict.fromArray([("tag", JSON.Encode.string("Renamed"))]))
|
|
28
24
|
|
|
29
25
|
let r1 = await ops.append(0, "id-1", [event1])
|
|
30
26
|
expect(r1)->toEqual(Ok())
|
|
@@ -98,9 +94,11 @@ describe("EventLogStorage_Sqlite", () => {
|
|
|
98
94
|
let ev = i => JSON.Encode.object(Dict.fromArray([("i", JSON.Encode.int(i))]))
|
|
99
95
|
// Seed one event, then stream three more starting at seq 1.
|
|
100
96
|
let _ = await ops.append(0, "id-s", [ev(0)])
|
|
101
|
-
let _ =
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
let _ = await ops.appendStream(
|
|
98
|
+
1,
|
|
99
|
+
"id-s",
|
|
100
|
+
Stream.fromIterable([ev(1), ev(2), ev(3)]),
|
|
101
|
+
)->Effect.runPromise
|
|
104
102
|
let replayed = await ops.replay("id-s")
|
|
105
103
|
expect(replayed->Array.length)->toBe(4)
|
|
106
104
|
expect(replayed->Array.getUnsafe(0))->toEqual(ev(0))
|
|
@@ -206,8 +204,9 @@ describe("EventLogStorage_Sqlite", () => {
|
|
|
206
204
|
testPromise("events persist across a reopen of the database file", async () => {
|
|
207
205
|
let path = `/tmp/reventless-test-eventlog-${Float.toString(Date.now())}.db`
|
|
208
206
|
|
|
209
|
-
// Session 1: write
|
|
210
207
|
{
|
|
208
|
+
// Session 1: write
|
|
209
|
+
|
|
211
210
|
module TestBus = LocalBus.Make()
|
|
212
211
|
module DbProvider = {
|
|
213
212
|
let db = SqliteDriver.openDb(~path)
|