@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
|
@@ -50,7 +50,12 @@ let makeHandler = () => {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// A DCB raw stored event with a deterministic msgId.
|
|
53
|
-
let dcbEvent = (
|
|
53
|
+
let dcbEvent = (
|
|
54
|
+
~eventType,
|
|
55
|
+
~tags,
|
|
56
|
+
~data=[],
|
|
57
|
+
~msgId,
|
|
58
|
+
): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
|
|
54
59
|
eventType,
|
|
55
60
|
data: JSON.Encode.object(Dict.fromArray(data)),
|
|
56
61
|
tags,
|
|
@@ -124,21 +129,22 @@ describe("ProjectionCheckpoint", () => {
|
|
|
124
129
|
},
|
|
125
130
|
)
|
|
126
131
|
|
|
127
|
-
testPromise(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
testPromise(
|
|
133
|
+
"appendStream batches never enter the pending set",
|
|
134
|
+
async () => {
|
|
135
|
+
ProjectionPending.reset()
|
|
136
|
+
ProjectionPending.enableTracking()
|
|
137
|
+
module TestBus = LocalBus.Make()
|
|
138
|
+
module DbProvider = {
|
|
139
|
+
let db = makeFreshDb()
|
|
140
|
+
}
|
|
141
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
142
|
+
let s = Storage.make(~name="agg", ~owner=None, ~opts)
|
|
143
|
+
let ops = await s.operations->TestRunner.resolve
|
|
144
|
+
|
|
145
|
+
// Bulk replay path: no publish cycle will ever resolve these, so they
|
|
146
|
+
// must not pin the low-watermark.
|
|
147
|
+
let _ = await ops.appendStream(
|
|
142
148
|
0,
|
|
143
149
|
"id-bulk",
|
|
144
150
|
Stream.fromIterable([
|
|
@@ -147,171 +153,211 @@ describe("ProjectionCheckpoint", () => {
|
|
|
147
153
|
]),
|
|
148
154
|
)->Effect.runPromise
|
|
149
155
|
|
|
150
|
-
|
|
156
|
+
expect(ProjectionPending.minPending(ProjectionPending.Aggregate))->toEqual(None)
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
// With nothing pending the watermark covers the bulk rows.
|
|
159
|
+
let db = DbProvider.db
|
|
160
|
+
ProjectionCheckpoint.setPosition(db, "RM", 0)
|
|
161
|
+
ProjectionCheckpoint.completePublished(db, [])
|
|
162
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM"))->toBe(2)
|
|
163
|
+
ProjectionPending.reset()
|
|
164
|
+
},
|
|
165
|
+
)
|
|
159
166
|
})
|
|
160
167
|
|
|
161
168
|
describe("catchupEnvelope", () => {
|
|
162
|
-
testSync(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
169
|
+
testSync(
|
|
170
|
+
"rebuilds the live {id, meta, event} envelope from a stored row",
|
|
171
|
+
() => {
|
|
172
|
+
let flat = flatEvent(
|
|
173
|
+
~id="p-1",
|
|
174
|
+
~seq=4,
|
|
175
|
+
~eventType="Created",
|
|
176
|
+
~data=[("name", JSON.Encode.string("Widget"))],
|
|
177
|
+
~msgId="m-1",
|
|
178
|
+
)
|
|
179
|
+
switch ProjectionCheckpoint.catchupEnvelope(flat) {
|
|
180
|
+
| None => expect("envelope")->toBe("None")
|
|
181
|
+
| Some(envelope) =>
|
|
182
|
+
let dict = envelope->JSON.Decode.object->Option.getOrThrow
|
|
183
|
+
expect(dict->Dict.get("id"))->toEqual(Some(JSON.Encode.string("p-1")))
|
|
184
|
+
expect(msgIdOfEnvelope(envelope))->toBe("m-1")
|
|
185
|
+
// Payload-bearing variant: TAG + payload fields (splitMessage reversed).
|
|
186
|
+
expect(dict->Dict.get("event"))->toEqual(
|
|
187
|
+
Some(
|
|
188
|
+
JSON.Encode.object(
|
|
189
|
+
Dict.fromArray([
|
|
190
|
+
("TAG", JSON.Encode.string("Created")),
|
|
191
|
+
("name", JSON.Encode.string("Widget")),
|
|
192
|
+
]),
|
|
193
|
+
),
|
|
184
194
|
),
|
|
185
|
-
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
testSync(
|
|
201
|
+
"payload-less variants come back as a bare JSON string",
|
|
202
|
+
() => {
|
|
203
|
+
let flat = flatEvent(~id="p-1", ~seq=0, ~eventType="Deleted", ~msgId="m-2")
|
|
204
|
+
let event =
|
|
205
|
+
ProjectionCheckpoint.catchupEnvelope(flat)
|
|
206
|
+
->Option.flatMap(JSON.Decode.object)
|
|
207
|
+
->Option.flatMap(d => d->Dict.get("event"))
|
|
208
|
+
expect(event)->toEqual(Some(JSON.Encode.string("Deleted")))
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
testSync(
|
|
213
|
+
"malformed stored rows yield None instead of throwing",
|
|
214
|
+
() => {
|
|
215
|
+
// Missing the required meta fields (composeMeta would throw).
|
|
216
|
+
let flat = JSON.Encode.object(
|
|
217
|
+
Dict.fromArray([
|
|
218
|
+
("id", JSON.Encode.string("p-1")),
|
|
219
|
+
("event", JSON.Encode.string("Created")),
|
|
220
|
+
]),
|
|
186
221
|
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let flat = flatEvent(~id="p-1", ~seq=0, ~eventType="Deleted", ~msgId="m-2")
|
|
192
|
-
let event =
|
|
193
|
-
ProjectionCheckpoint.catchupEnvelope(flat)
|
|
194
|
-
->Option.flatMap(JSON.Decode.object)
|
|
195
|
-
->Option.flatMap(d => d->Dict.get("event"))
|
|
196
|
-
expect(event)->toEqual(Some(JSON.Encode.string("Deleted")))
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
testSync("malformed stored rows yield None instead of throwing", () => {
|
|
200
|
-
// Missing the required meta fields (composeMeta would throw).
|
|
201
|
-
let flat = JSON.Encode.object(
|
|
202
|
-
Dict.fromArray([
|
|
203
|
-
("id", JSON.Encode.string("p-1")),
|
|
204
|
-
("event", JSON.Encode.string("Created")),
|
|
205
|
-
]),
|
|
206
|
-
)
|
|
207
|
-
expect(ProjectionCheckpoint.catchupEnvelope(flat))->toEqual(None)
|
|
208
|
-
expect(ProjectionCheckpoint.catchupEnvelope(JSON.Encode.string("junk")))->toEqual(None)
|
|
209
|
-
})
|
|
222
|
+
expect(ProjectionCheckpoint.catchupEnvelope(flat))->toEqual(None)
|
|
223
|
+
expect(ProjectionCheckpoint.catchupEnvelope(JSON.Encode.string("junk")))->toEqual(None)
|
|
224
|
+
},
|
|
225
|
+
)
|
|
210
226
|
})
|
|
211
227
|
|
|
212
228
|
describe("runCatchup", () => {
|
|
213
|
-
testPromise(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
~upperBound,
|
|
238
|
-
~dcbUpperBound=0,
|
|
239
|
-
~handlers=[("RM-A", handlerA), ("RM-B", handlerB)],
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
expect(receivedA->Array.map(msgIdOfEnvelope))->toEqual(["m1", "m2", "m3"])
|
|
243
|
-
expect(receivedB->Array.map(msgIdOfEnvelope))->toEqual(["m1", "m2", "m3"])
|
|
244
|
-
expect(ProjectionCheckpoint.getPosition(db, "RM-A"))->toBe(3)
|
|
245
|
-
expect(ProjectionCheckpoint.getPosition(db, "RM-B"))->toBe(3)
|
|
246
|
-
|
|
247
|
-
// Second startup: checkpoints are current — nothing is redelivered.
|
|
248
|
-
await ProjectionCheckpoint.runCatchup(
|
|
249
|
-
~db,
|
|
250
|
-
~upperBound=ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate),
|
|
251
|
-
~dcbUpperBound=0,
|
|
252
|
-
~handlers=[("RM-A", handlerA), ("RM-B", handlerB)],
|
|
253
|
-
)
|
|
254
|
-
expect(receivedA->Array.length)->toBe(3)
|
|
255
|
-
expect(receivedB->Array.length)->toBe(3)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
testPromise("a fresh database still stamps checkpoint rows at 0", async () => {
|
|
259
|
-
// Regression (caught by the platform smoke run): with upperBound = 0 and
|
|
260
|
-
// no events, runCatchup must still CREATE the rows — the runtime
|
|
261
|
-
// advanceAll only lifts existing rows, so a missing row would make every
|
|
262
|
-
// subsequent startup redeliver the whole history.
|
|
263
|
-
ProjectionPending.reset()
|
|
264
|
-
let db = makeFreshDb()
|
|
265
|
-
let (_, handler) = makeHandler()
|
|
266
|
-
await ProjectionCheckpoint.runCatchup(~db, ~upperBound=0, ~dcbUpperBound=0, ~handlers=[("RM-Fresh", handler)])
|
|
267
|
-
let stamped =
|
|
268
|
-
db
|
|
269
|
-
->SqliteDriver.prepare(
|
|
270
|
-
"SELECT COUNT(*) AS c FROM projection_checkpoint WHERE read_model = ?",
|
|
229
|
+
testPromise(
|
|
230
|
+
"delivers the missed range in rowid order, exactly once",
|
|
231
|
+
async () => {
|
|
232
|
+
ProjectionPending.reset()
|
|
233
|
+
module TestBus = LocalBus.Make()
|
|
234
|
+
module DbProvider = {
|
|
235
|
+
let db = makeFreshDb()
|
|
236
|
+
}
|
|
237
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
238
|
+
let s = Storage.make(~name="agg", ~owner=None, ~opts)
|
|
239
|
+
let ops = await s.operations->TestRunner.resolve
|
|
240
|
+
let db = DbProvider.db
|
|
241
|
+
|
|
242
|
+
// Interleaved appends across two aggregates — global order is rowid
|
|
243
|
+
// order (m1, m2, m3), not per-aggregate grouping.
|
|
244
|
+
let _ = await ops.append(
|
|
245
|
+
0,
|
|
246
|
+
"id-1",
|
|
247
|
+
[flatEvent(~id="id-1", ~seq=0, ~eventType="Created", ~msgId="m1")],
|
|
248
|
+
)
|
|
249
|
+
let _ = await ops.append(
|
|
250
|
+
0,
|
|
251
|
+
"id-2",
|
|
252
|
+
[flatEvent(~id="id-2", ~seq=0, ~eventType="Created", ~msgId="m2")],
|
|
271
253
|
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
254
|
+
let _ = await ops.append(
|
|
255
|
+
1,
|
|
256
|
+
"id-1",
|
|
257
|
+
[flatEvent(~id="id-1", ~seq=1, ~eventType="Renamed", ~msgId="m3")],
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
let (receivedA, handlerA) = makeHandler()
|
|
261
|
+
let (receivedB, handlerB) = makeHandler()
|
|
262
|
+
let upperBound = ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate)
|
|
263
|
+
expect(upperBound)->toBe(3)
|
|
264
|
+
|
|
265
|
+
await ProjectionCheckpoint.runCatchup(
|
|
266
|
+
~db,
|
|
267
|
+
~upperBound,
|
|
268
|
+
~dcbUpperBound=0,
|
|
269
|
+
~handlers=[("RM-A", handlerA), ("RM-B", handlerB)],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
expect(receivedA->Array.map(msgIdOfEnvelope))->toEqual(["m1", "m2", "m3"])
|
|
273
|
+
expect(receivedB->Array.map(msgIdOfEnvelope))->toEqual(["m1", "m2", "m3"])
|
|
274
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM-A"))->toBe(3)
|
|
275
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM-B"))->toBe(3)
|
|
276
|
+
|
|
277
|
+
// Second startup: checkpoints are current — nothing is redelivered.
|
|
278
|
+
await ProjectionCheckpoint.runCatchup(
|
|
279
|
+
~db,
|
|
280
|
+
~upperBound=ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate),
|
|
281
|
+
~dcbUpperBound=0,
|
|
282
|
+
~handlers=[("RM-A", handlerA), ("RM-B", handlerB)],
|
|
283
|
+
)
|
|
284
|
+
expect(receivedA->Array.length)->toBe(3)
|
|
285
|
+
expect(receivedB->Array.length)->toBe(3)
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
testPromise(
|
|
290
|
+
"a fresh database still stamps checkpoint rows at 0",
|
|
291
|
+
async () => {
|
|
292
|
+
// Regression (caught by the platform smoke run): with upperBound = 0 and
|
|
293
|
+
// no events, runCatchup must still CREATE the rows — the runtime
|
|
294
|
+
// advanceAll only lifts existing rows, so a missing row would make every
|
|
295
|
+
// subsequent startup redeliver the whole history.
|
|
296
|
+
ProjectionPending.reset()
|
|
283
297
|
let db = makeFreshDb()
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
298
|
+
let (_, handler) = makeHandler()
|
|
299
|
+
await ProjectionCheckpoint.runCatchup(
|
|
300
|
+
~db,
|
|
301
|
+
~upperBound=0,
|
|
302
|
+
~dcbUpperBound=0,
|
|
303
|
+
~handlers=[("RM-Fresh", handler)],
|
|
304
|
+
)
|
|
305
|
+
let stamped =
|
|
306
|
+
db
|
|
307
|
+
->SqliteDriver.prepare(
|
|
308
|
+
"SELECT COUNT(*) AS c FROM projection_checkpoint WHERE read_model = ?",
|
|
309
|
+
)
|
|
310
|
+
->SqliteDriver.get([JSON.Encode.string("RM-Fresh")])
|
|
311
|
+
expect(stamped->Option.flatMap(r => r->Dict.get("c")))->toEqual(
|
|
312
|
+
Some(JSON.Encode.float(1.0)),
|
|
313
|
+
)
|
|
314
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM-Fresh"))->toBe(0)
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
testPromise(
|
|
319
|
+
"a read model without a checkpoint row is seeded from history",
|
|
320
|
+
async () => {
|
|
321
|
+
ProjectionPending.reset()
|
|
322
|
+
module TestBus = LocalBus.Make()
|
|
323
|
+
module DbProvider = {
|
|
324
|
+
let db = makeFreshDb()
|
|
325
|
+
}
|
|
326
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
327
|
+
let s = Storage.make(~name="agg", ~owner=None, ~opts)
|
|
328
|
+
let ops = await s.operations->TestRunner.resolve
|
|
329
|
+
let db = DbProvider.db
|
|
330
|
+
|
|
331
|
+
let _ = await ops.append(
|
|
332
|
+
0,
|
|
333
|
+
"id-1",
|
|
334
|
+
[flatEvent(~id="id-1", ~seq=0, ~eventType="Created", ~msgId="m1")],
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
// First session knows only RM-A.
|
|
338
|
+
let (receivedA, handlerA) = makeHandler()
|
|
339
|
+
await ProjectionCheckpoint.runCatchup(
|
|
340
|
+
~db,
|
|
341
|
+
~upperBound=ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate),
|
|
342
|
+
~dcbUpperBound=0,
|
|
343
|
+
~handlers=[("RM-A", handlerA)],
|
|
344
|
+
)
|
|
345
|
+
expect(receivedA->Array.length)->toBe(1)
|
|
346
|
+
|
|
347
|
+
// A read model added later starts at 0 and receives the full history;
|
|
348
|
+
// the existing one stays quiet.
|
|
349
|
+
let (receivedNew, handlerNew) = makeHandler()
|
|
350
|
+
await ProjectionCheckpoint.runCatchup(
|
|
351
|
+
~db,
|
|
352
|
+
~upperBound=ProjectionCheckpoint.maxPosition(db, ProjectionPending.Aggregate),
|
|
353
|
+
~dcbUpperBound=0,
|
|
354
|
+
~handlers=[("RM-A", handlerA), ("RM-New", handlerNew)],
|
|
355
|
+
)
|
|
356
|
+
expect(receivedA->Array.length)->toBe(1)
|
|
357
|
+
expect(receivedNew->Array.map(msgIdOfEnvelope))->toEqual(["m1"])
|
|
358
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM-New"))->toBe(1)
|
|
359
|
+
},
|
|
360
|
+
)
|
|
315
361
|
})
|
|
316
362
|
|
|
317
363
|
describe("DCB axis", () => {
|
|
@@ -382,41 +428,50 @@ describe("ProjectionCheckpoint", () => {
|
|
|
382
428
|
},
|
|
383
429
|
)
|
|
384
430
|
|
|
385
|
-
testPromise(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
431
|
+
testPromise(
|
|
432
|
+
"the two axes' watermarks are independent",
|
|
433
|
+
async () => {
|
|
434
|
+
ProjectionPending.reset()
|
|
435
|
+
ProjectionPending.enableTracking()
|
|
436
|
+
module TestBus = LocalBus.Make()
|
|
437
|
+
module DbProvider = {
|
|
438
|
+
let db = makeFreshDb()
|
|
439
|
+
}
|
|
440
|
+
module AggStorage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
441
|
+
let aggS = AggStorage.make(~name="agg", ~owner=None, ~opts)
|
|
442
|
+
let aggOps = await aggS.operations->TestRunner.resolve
|
|
443
|
+
let db = DbProvider.db
|
|
444
|
+
let dcbOps = await makeDcbStorage(db, ~name="dcb-log")
|
|
445
|
+
|
|
446
|
+
// One pending append on each axis.
|
|
447
|
+
let _ = await aggOps.append(
|
|
448
|
+
0,
|
|
449
|
+
"id-1",
|
|
450
|
+
[flatEvent(~id="id-1", ~seq=0, ~eventType="Created", ~msgId="agg1")],
|
|
451
|
+
)
|
|
452
|
+
let _ = await dcbOps.append(
|
|
453
|
+
[
|
|
454
|
+
dcbEvent(
|
|
455
|
+
~eventType="OrderPlaced",
|
|
456
|
+
~tags=[{key: "orderId", value: "o-1"}],
|
|
457
|
+
~msgId="dcb1",
|
|
458
|
+
),
|
|
459
|
+
],
|
|
460
|
+
~condition=?None,
|
|
461
|
+
)
|
|
462
|
+
ProjectionCheckpoint.setPosition(db, "RM", 0)
|
|
463
|
+
ProjectionCheckpoint.setPosition(db, "dcb:RM", 0)
|
|
464
|
+
|
|
465
|
+
// Only the DCB batch completes: its axis advances, the aggregate axis
|
|
466
|
+
// stays capped by its own pending append.
|
|
467
|
+
ProjectionCheckpoint.completePublished(db, ["dcb1"])
|
|
468
|
+
expect(ProjectionCheckpoint.getPosition(db, "dcb:RM"))->toBe(1)
|
|
469
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM"))->toBe(0)
|
|
470
|
+
|
|
471
|
+
ProjectionCheckpoint.completePublished(db, ["agg1"])
|
|
472
|
+
expect(ProjectionCheckpoint.getPosition(db, "RM"))->toBe(1)
|
|
473
|
+
ProjectionPending.reset()
|
|
474
|
+
},
|
|
475
|
+
)
|
|
421
476
|
})
|
|
422
477
|
})
|