@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -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 +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- 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.mjs +1 -1
- 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 +9 -5
- 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
|
@@ -14,90 +14,117 @@ describe("DcbEventLog.readStream (in-memory adapter)", () => {
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
describe("basic streaming", () => {
|
|
17
|
-
testPromise(
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
testPromise(
|
|
18
|
+
"readStream for tag with no matching events returns empty stream",
|
|
19
|
+
async () => {
|
|
20
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
21
|
+
let count = await ops.readStream(~query=tagQuery("no-such-id"))
|
|
20
22
|
->Stream.runFold(0, (n, _) => n + 1)
|
|
21
23
|
->Effect.runPromise
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
expect(count)->toBe(0)
|
|
25
|
+
},
|
|
26
|
+
)
|
|
24
27
|
|
|
25
|
-
testPromise(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
testPromise(
|
|
29
|
+
"readStream emits all events matching a tag query",
|
|
30
|
+
async () => {
|
|
31
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
32
|
+
let _ = await dispatch(addItemJson("stream-1", "Widget"), "stream-1")
|
|
33
|
+
let count = await ops.readStream(~query=tagQuery("stream-1"))
|
|
29
34
|
->Stream.runFold(0, (n, _) => n + 1)
|
|
30
35
|
->Effect.runPromise
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
expect(count)->toBe(1)
|
|
37
|
+
},
|
|
38
|
+
)
|
|
33
39
|
|
|
34
|
-
testPromise(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
testPromise(
|
|
41
|
+
"readStream for distinct IDs returns independent streams",
|
|
42
|
+
async () => {
|
|
43
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
44
|
+
let _ = await dispatch(addItemJson("stream-A", "Alpha"), "stream-A")
|
|
45
|
+
let _ = await dispatch(addItemJson("stream-B", "Beta"), "stream-B")
|
|
46
|
+
let countA = await ops.readStream(~query=tagQuery("stream-A"))
|
|
39
47
|
->Stream.runFold(0, (n, _) => n + 1)
|
|
40
48
|
->Effect.runPromise
|
|
41
|
-
|
|
49
|
+
let countB = await ops.readStream(~query=tagQuery("stream-B"))
|
|
42
50
|
->Stream.runFold(0, (n, _) => n + 1)
|
|
43
51
|
->Effect.runPromise
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
expect(countA)->toBe(1)
|
|
53
|
+
expect(countB)->toBe(1)
|
|
54
|
+
},
|
|
55
|
+
)
|
|
47
56
|
|
|
48
|
-
testPromise(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
testPromise(
|
|
58
|
+
"take(1) limits stream without loading all",
|
|
59
|
+
async () => {
|
|
60
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
61
|
+
let _ = await dispatch(addItemJson("stream-C1", "C1"), "stream-C1")
|
|
62
|
+
let _ = await dispatch(addItemJson("stream-C2", "C2"), "stream-C2")
|
|
63
|
+
// Broad query: all ItemAdded events regardless of tag value
|
|
64
|
+
let result = await ops.readStream(~query=typeQuery("ItemAdded"))
|
|
54
65
|
->Stream.take(1)
|
|
55
66
|
->Stream.runCollect
|
|
56
67
|
->Effect.runPromise
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
expect(result->Array.length)->toBe(1)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
59
71
|
})
|
|
60
72
|
|
|
61
73
|
describe("fold patterns — (state, headPosition) used in StateChangeSlice_Callback", () => {
|
|
62
|
-
testPromise(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
testPromise(
|
|
75
|
+
"runFold extracts last headPosition for append condition",
|
|
76
|
+
async () => {
|
|
77
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
78
|
+
let _ = await dispatch(addItemJson("fold-1", "FoldItem"), "fold-1")
|
|
79
|
+
let (_, headPosition) = await ops.readStream(~query=tagQuery("fold-1"))
|
|
66
80
|
->Stream.runFold(((), None), ((_dm, _pos), se) => ((), Some(se.position)))
|
|
67
81
|
->Effect.runPromise
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
expect(Option.isSome(headPosition))->toBe(true)
|
|
83
|
+
},
|
|
84
|
+
)
|
|
70
85
|
|
|
71
|
-
testPromise(
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
testPromise(
|
|
87
|
+
"runFold on empty stream returns initial accumulator",
|
|
88
|
+
async () => {
|
|
89
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
90
|
+
let (_, headPosition) = await ops.readStream(~query=tagQuery("no-such-id-fold"))
|
|
74
91
|
->Stream.runFold((false, None), ((_dm, _pos), se) => (true, Some(se.position)))
|
|
75
92
|
->Effect.runPromise
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
expect(headPosition)->toEqual(None)
|
|
94
|
+
},
|
|
95
|
+
)
|
|
78
96
|
})
|
|
79
97
|
|
|
80
|
-
describe(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
describe(
|
|
99
|
+
"StateChangeSlice regression — second command sees first event via stream fold",
|
|
100
|
+
() => {
|
|
101
|
+
testPromise(
|
|
102
|
+
"second AddItem with same ID is rejected (state sees existing event)",
|
|
103
|
+
async () => {
|
|
104
|
+
// First command succeeds → ItemAdded event stored
|
|
105
|
+
// Second command: readStream folds all events → state = true (exists)
|
|
106
|
+
// decide(true, AddItem) → Error(ItemAlreadyExists) → no new events
|
|
107
|
+
let id = "dup-test-stream"
|
|
108
|
+
let _ = await dispatch(addItemJson(id, "First"), id)
|
|
109
|
+
let countBefore = capturedEventCount.contents
|
|
110
|
+
let _ = await dispatch(addItemJson(id, "Duplicate"), id)
|
|
111
|
+
// Duplicate command must not publish a second event
|
|
112
|
+
expect(capturedEventCount.contents)->toBe(countBefore)
|
|
113
|
+
},
|
|
114
|
+
)
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
testPromise(
|
|
117
|
+
"second AddItem with different ID succeeds",
|
|
118
|
+
async () => {
|
|
119
|
+
let id1 = "distinct-1"
|
|
120
|
+
let id2 = "distinct-2"
|
|
121
|
+
let _ = await dispatch(addItemJson(id1, "First"), id1)
|
|
122
|
+
let countAfterFirst = capturedEventCount.contents
|
|
123
|
+
let _ = await dispatch(addItemJson(id2, "Second"), id2)
|
|
124
|
+
// New ID should publish an event
|
|
125
|
+
expect(capturedEventCount.contents)->toBe(countAfterFirst + 1)
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
)
|
|
103
130
|
})
|
|
@@ -78,7 +78,10 @@ let _ = TestRunner.setup()
|
|
|
78
78
|
// ─────────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
80
|
module ItemEventLogMaker = DcbEventLog_Builder.Make(Bus)
|
|
81
|
-
let eventLog = ItemEventLogMaker.make(
|
|
81
|
+
let eventLog = ItemEventLogMaker.make(
|
|
82
|
+
~name="ItemEventLog",
|
|
83
|
+
~partitionTag=Reventless.DcbTag.Simple({key: "id"}),
|
|
84
|
+
)
|
|
82
85
|
|
|
83
86
|
// ─────────────────────────────────────────────────────────────
|
|
84
87
|
// Build StateChangeSlice
|
|
@@ -17,8 +17,9 @@ describe("DcbEventLog E2E", () => {
|
|
|
17
17
|
|
|
18
18
|
testPromise("AddItem command publishes 1 event to event topic", async () => {
|
|
19
19
|
let cmd =
|
|
20
|
-
AddItemSpec.AddItem({id: "item-1", name: "Widget"})
|
|
21
|
-
|
|
20
|
+
AddItemSpec.AddItem({id: "item-1", name: "Widget"})->ReventlessCore.Message.encode(
|
|
21
|
+
AddItemSpec.commandSchema,
|
|
22
|
+
)
|
|
22
23
|
await dispatch(cmd, "item-1")
|
|
23
24
|
expect(capturedEventCount.contents)->toBe(1)
|
|
24
25
|
})
|
|
@@ -26,16 +27,18 @@ describe("DcbEventLog E2E", () => {
|
|
|
26
27
|
testPromise("duplicate AddItem produces 0 events (ItemAlreadyExists)", async () => {
|
|
27
28
|
// item-1 was already created in the previous test — same aggregate state persists
|
|
28
29
|
let cmd =
|
|
29
|
-
AddItemSpec.AddItem({id: "item-1", name: "Widget"})
|
|
30
|
-
|
|
30
|
+
AddItemSpec.AddItem({id: "item-1", name: "Widget"})->ReventlessCore.Message.encode(
|
|
31
|
+
AddItemSpec.commandSchema,
|
|
32
|
+
)
|
|
31
33
|
await dispatch(cmd, "item-1")
|
|
32
34
|
expect(capturedEventCount.contents)->toBe(0)
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
testPromise("AddItem for new id produces 1 event", async () => {
|
|
36
38
|
let cmd =
|
|
37
|
-
AddItemSpec.AddItem({id: "item-2", name: "Gadget"})
|
|
38
|
-
|
|
39
|
+
AddItemSpec.AddItem({id: "item-2", name: "Gadget"})->ReventlessCore.Message.encode(
|
|
40
|
+
AddItemSpec.commandSchema,
|
|
41
|
+
)
|
|
39
42
|
await dispatch(cmd, "item-2")
|
|
40
43
|
expect(capturedEventCount.contents)->toBe(1)
|
|
41
44
|
})
|
|
@@ -27,9 +27,12 @@ describe("EventCollector (in-memory)", () => {
|
|
|
27
27
|
// The EventCollector itself doesn't expose a direct handler in our test setup —
|
|
28
28
|
// verify the bus subscription mechanism works
|
|
29
29
|
let received: ref<int> = ref(0)
|
|
30
|
-
Bus.subscribeToEvents(
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Bus.subscribeToEvents(
|
|
31
|
+
topicName,
|
|
32
|
+
async (_, _, _) => {
|
|
33
|
+
received := received.contents + 1
|
|
34
|
+
},
|
|
35
|
+
)
|
|
33
36
|
let testEvent = JSON.Encode.object(Dict.fromArray([("type", JSON.Encode.string("TestEvent"))]))
|
|
34
37
|
await Bus.publishEvent(topicName, "test", testMeta, testEvent)
|
|
35
38
|
expect(received.contents)->toBe(1)
|
|
@@ -14,80 +14,100 @@ describe("EventLog.appendStream (in-memory adapter)", () => {
|
|
|
14
14
|
let _ = beforeEach(() => reset())
|
|
15
15
|
|
|
16
16
|
describe("basic append", () => {
|
|
17
|
-
testPromise(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
testPromise(
|
|
18
|
+
"appendStream writes all events to storage",
|
|
19
|
+
async () => {
|
|
20
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
21
|
+
let stream =
|
|
22
|
+
[
|
|
23
|
+
ItemEventLogSpec.ItemCreated({name: "e1"}),
|
|
24
|
+
ItemEventLogSpec.ItemCreated({name: "e2"}),
|
|
25
|
+
ItemEventLogSpec.ItemCreated({name: "e3"}),
|
|
26
|
+
]->Stream.fromIterable
|
|
27
|
+
let _ = await ops.appendStream(0, "as-basic-1", stream)->Effect.runPromise
|
|
28
|
+
let replayed = await ops.replay("as-basic-1")
|
|
29
|
+
expect(replayed->Array.length)->toBe(3)
|
|
30
|
+
},
|
|
31
|
+
)
|
|
28
32
|
|
|
29
|
-
testPromise(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
testPromise(
|
|
34
|
+
"appendStream on empty stream writes nothing",
|
|
35
|
+
async () => {
|
|
36
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
37
|
+
let _ = await ops.appendStream(0, "as-empty-1", Stream.empty)->Effect.runPromise
|
|
38
|
+
let replayed = await ops.replay("as-empty-1")
|
|
39
|
+
expect(replayed->Array.length)->toBe(0)
|
|
40
|
+
},
|
|
41
|
+
)
|
|
35
42
|
|
|
36
|
-
testPromise(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
testPromise(
|
|
44
|
+
"appendStream preserves event order",
|
|
45
|
+
async () => {
|
|
46
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
47
|
+
let stream =
|
|
48
|
+
[
|
|
49
|
+
ItemEventLogSpec.ItemCreated({name: "first"}),
|
|
50
|
+
ItemEventLogSpec.ItemDeleted({id: "second"}),
|
|
51
|
+
]->Stream.fromIterable
|
|
52
|
+
let _ = await ops.appendStream(0, "as-order-1", stream)->Effect.runPromise
|
|
53
|
+
let replayed = await ops.replayStream("as-order-1")->Stream.runCollect->Effect.runPromise
|
|
54
|
+
let first = replayed->Array.getUnsafe(0)
|
|
55
|
+
expect(first)->toEqual(ItemEventLogSpec.ItemCreated({name: "first"}))
|
|
56
|
+
},
|
|
57
|
+
)
|
|
47
58
|
|
|
48
|
-
testPromise(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
testPromise(
|
|
60
|
+
"appendStream after array append continues from correct sequenceNr",
|
|
61
|
+
async () => {
|
|
62
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
63
|
+
let _ = await ops.append(
|
|
64
|
+
0,
|
|
65
|
+
"as-seq-1",
|
|
66
|
+
[makeEvent'("as-seq-1", ItemEventLogSpec.ItemCreated({name: "initial"}))],
|
|
67
|
+
)
|
|
68
|
+
let stream = [ItemEventLogSpec.ItemCreated({name: "streamed"})]->Stream.fromIterable
|
|
69
|
+
let _ = await ops.appendStream(1, "as-seq-1", stream)->Effect.runPromise
|
|
70
|
+
let replayed = await ops.replay("as-seq-1")
|
|
71
|
+
expect(replayed->Array.length)->toBe(2)
|
|
72
|
+
},
|
|
73
|
+
)
|
|
60
74
|
})
|
|
61
75
|
|
|
62
76
|
describe("replayStream → appendStream pipeline", () => {
|
|
63
|
-
testPromise(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
testPromise(
|
|
78
|
+
"can copy events from one aggregate to another via streams",
|
|
79
|
+
async () => {
|
|
80
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
81
|
+
let _ = await ops.append(
|
|
82
|
+
0,
|
|
83
|
+
"as-src-1",
|
|
84
|
+
[
|
|
85
|
+
makeEvent'("as-src-1", ItemEventLogSpec.ItemCreated({name: "a"})),
|
|
86
|
+
makeEvent'("as-src-1", ItemEventLogSpec.ItemDeleted({id: "b"})),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
let sourceStream = ops.replayStream("as-src-1")
|
|
90
|
+
let _ = await ops.appendStream(0, "as-dst-1", sourceStream)->Effect.runPromise
|
|
91
|
+
let destEvents = await ops.replay("as-dst-1")
|
|
92
|
+
expect(destEvents->Array.length)->toBe(2)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
78
95
|
|
|
79
|
-
testPromise(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
testPromise(
|
|
97
|
+
"copied events round-trip through decode correctly",
|
|
98
|
+
async () => {
|
|
99
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
100
|
+
let _ = await ops.append(
|
|
101
|
+
0,
|
|
102
|
+
"as-src-2",
|
|
103
|
+
[makeEvent'("as-src-2", ItemEventLogSpec.ItemCreated({name: "copied"}))],
|
|
104
|
+
)
|
|
105
|
+
let sourceStream = ops.replayStream("as-src-2")
|
|
106
|
+
let _ = await ops.appendStream(0, "as-dst-2", sourceStream)->Effect.runPromise
|
|
107
|
+
let destEvents = await ops.replayStream("as-dst-2")->Stream.runCollect->Effect.runPromise
|
|
108
|
+
let first = destEvents->Array.getUnsafe(0)
|
|
109
|
+
expect(first)->toEqual(ItemEventLogSpec.ItemCreated({name: "copied"}))
|
|
110
|
+
},
|
|
111
|
+
)
|
|
92
112
|
})
|
|
93
113
|
})
|
|
@@ -75,10 +75,14 @@ describe("EventLog — conflict detection (in-memory)", () => {
|
|
|
75
75
|
|
|
76
76
|
testPromise("batch append with correct sequenceNr stores all events", async () => {
|
|
77
77
|
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
78
|
-
let result = await ops.append(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
let result = await ops.append(
|
|
79
|
+
0,
|
|
80
|
+
"conflict-5",
|
|
81
|
+
[
|
|
82
|
+
makeEvent'("conflict-5", ItemEventLogSpec.ItemCreated({name: "e1"})),
|
|
83
|
+
makeEvent'("conflict-5", ItemEventLogSpec.ItemDeleted({id: "e2"})),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
82
86
|
expect(Result.isOk(result))->toBe(true)
|
|
83
87
|
let replayed = await ops.replay("conflict-5")
|
|
84
88
|
expect(replayed->Array.length)->toBe(2)
|
|
@@ -11,7 +11,7 @@ module ItemEventLogSpec = {
|
|
|
11
11
|
let name = "TestItemEventLog"
|
|
12
12
|
|
|
13
13
|
@schema
|
|
14
|
-
type event =
|
|
14
|
+
type event = ItemCreated({name: string}) | ItemDeleted({id: string})
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -41,11 +41,11 @@ module EventLogMaker = ReventlessCore.EventLog_Builder.Make(
|
|
|
41
41
|
|
|
42
42
|
let eventLog = EventLogMaker.make(~name="TestItemEventLog")
|
|
43
43
|
|
|
44
|
-
let makeEvent' = (id, event) =>
|
|
44
|
+
let makeEvent' = (id, event): Reventless.Message.event'<string, ItemEventLogSpec.event> => {
|
|
45
45
|
Reventless.Message.id,
|
|
46
46
|
meta: testMeta,
|
|
47
47
|
event,
|
|
48
|
-
}
|
|
48
|
+
}
|
|
49
49
|
|
|
50
50
|
let reset = () => {
|
|
51
51
|
capturedTopicEventCount := 0
|
|
@@ -30,7 +30,9 @@ module Recorder: ReventlessCore.EventLogProvisioning.Backend = {
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
ReventlessCore.EventLogProvisioning.use(
|
|
33
|
+
ReventlessCore.EventLogProvisioning.use(
|
|
34
|
+
module(Recorder: ReventlessCore.EventLogProvisioning.Backend),
|
|
35
|
+
)
|
|
34
36
|
|
|
35
37
|
module Bus = LocalBus.Make()
|
|
36
38
|
|
|
@@ -42,7 +44,7 @@ module SeamItemSpec = {
|
|
|
42
44
|
let name = "SeamItemEventLog"
|
|
43
45
|
|
|
44
46
|
@schema
|
|
45
|
-
type event =
|
|
47
|
+
type event = SeamItemCreated({name: string})
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
module ClassicMaker = ReventlessCore.EventLog_Builder.Make(
|
|
@@ -53,7 +55,10 @@ module ClassicMaker = ReventlessCore.EventLog_Builder.Make(
|
|
|
53
55
|
|
|
54
56
|
// The plugin builder publishes this context around a plugin's construct; entering
|
|
55
57
|
// it here is what a real plugin build does, so the seam should carry the names.
|
|
56
|
-
let previousContext = ReventlessCore.ResourceAttribution.enter(
|
|
58
|
+
let previousContext = ReventlessCore.ResourceAttribution.enter(
|
|
59
|
+
~platform="seam-platform",
|
|
60
|
+
~plugin="SeamPlugin",
|
|
61
|
+
)
|
|
57
62
|
|
|
58
63
|
let classicLog = ClassicMaker.make(
|
|
59
64
|
~name="SeamItem",
|
|
@@ -63,10 +68,7 @@ let classicLog = ClassicMaker.make(
|
|
|
63
68
|
// ── DCB log ──────────────────────────────────────────────────
|
|
64
69
|
module DcbMaker = DcbEventLog_Builder.Make(Bus)
|
|
65
70
|
|
|
66
|
-
let dcbLog = DcbMaker.make(
|
|
67
|
-
~name="SeamCatalog",
|
|
68
|
-
~partitionTag=Reventless.DcbTag.Simple({key: "id"}),
|
|
69
|
-
)
|
|
71
|
+
let dcbLog = DcbMaker.make(~name="SeamCatalog", ~partitionTag=Reventless.DcbTag.Simple({key: "id"}))
|
|
70
72
|
|
|
71
73
|
ReventlessCore.ResourceAttribution.restore(previousContext)
|
|
72
74
|
|
|
@@ -80,16 +82,27 @@ describe("EventLogProvisioning seam, driven by the real builders", () => {
|
|
|
80
82
|
|
|
81
83
|
testSync("carries the owning element: the aggregate for classic, the plugin for DCB", () => {
|
|
82
84
|
expect(provisioned->Array.map(r => r.owner))->toEqual([
|
|
83
|
-
Some(
|
|
84
|
-
|
|
85
|
+
Some(
|
|
86
|
+
(
|
|
87
|
+
{
|
|
88
|
+
kind: ReventlessCore.ComponentType.Aggregate,
|
|
89
|
+
name: "SeamItem",
|
|
90
|
+
}: ReventlessCore.ResourceAttribution.owner
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
Some(
|
|
94
|
+
(
|
|
95
|
+
{
|
|
96
|
+
kind: ReventlessCore.ComponentType.Plugin,
|
|
97
|
+
name: "SeamCatalog",
|
|
98
|
+
}: ReventlessCore.ResourceAttribution.owner
|
|
99
|
+
),
|
|
100
|
+
),
|
|
85
101
|
])
|
|
86
102
|
})
|
|
87
103
|
|
|
88
104
|
testSync("carries the ambient plugin attribution the builder published", () => {
|
|
89
|
-
expect(provisioned->Array.map(r => r.plugin))->toEqual([
|
|
90
|
-
Some("SeamPlugin"),
|
|
91
|
-
Some("SeamPlugin"),
|
|
92
|
-
])
|
|
105
|
+
expect(provisioned->Array.map(r => r.plugin))->toEqual([Some("SeamPlugin"), Some("SeamPlugin")])
|
|
93
106
|
})
|
|
94
107
|
|
|
95
108
|
testSync("carries the adapter's resources — empty for in-memory storage", () => {
|