@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,8 +14,7 @@ module OrderAggregateSource = {
|
|
|
14
14
|
module Id = Id.String
|
|
15
15
|
let name = "OrderAggregateEventTopic"
|
|
16
16
|
@schema
|
|
17
|
-
type event =
|
|
18
|
-
| OrderShipped({orderId: string, productId: string})
|
|
17
|
+
type event = OrderShipped({orderId: string, productId: string})
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -47,10 +46,11 @@ module AutoFulfillSpec = {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
@schema
|
|
50
|
-
type command =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
type command =
|
|
50
|
+
| MarkFulfilled({
|
|
51
|
+
orderId: @s.matches(DcbTag.string) string,
|
|
52
|
+
productId: @s.matches(DcbTag.string) string,
|
|
53
|
+
})
|
|
54
54
|
|
|
55
55
|
let maxRetries = 3
|
|
56
56
|
let heartbeatInterval = 60
|
|
@@ -122,10 +122,11 @@ module FromInventoryDcb = AutomationSlice.Mapping.Make(
|
|
|
122
122
|
// Mappings collection
|
|
123
123
|
// ─────────────────────────────────────────────────────────────
|
|
124
124
|
|
|
125
|
-
module AutoFulfillAutomation: AutomationSlice.Automation
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
module AutoFulfillAutomation: AutomationSlice.Automation with module Spec := AutoFulfillSpec = {
|
|
126
|
+
let process = (id, item: AutoFulfillSpec.todoItem) => Some((
|
|
127
|
+
id,
|
|
128
|
+
AutoFulfillSpec.MarkFulfilled({orderId: item.orderId, productId: item.productId}),
|
|
129
|
+
))
|
|
129
130
|
let onExhausted = (_id, _item: AutoFulfillSpec.todoItem) => None
|
|
130
131
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
131
132
|
module M = AutomationSlice.Mappings.Make(AutoFulfillSpec)
|
|
@@ -168,10 +169,7 @@ let _ = TestRunner.setup()
|
|
|
168
169
|
// ─────────────────────────────────────────────────────────────
|
|
169
170
|
|
|
170
171
|
module AutomationSliceMaker = AutomationSlice_Builder.Make(Bus)
|
|
171
|
-
module AutoFulfill = AutomationSliceMaker.Make(
|
|
172
|
-
AutoFulfillSpec,
|
|
173
|
-
AutoFulfillAutomation,
|
|
174
|
-
)
|
|
172
|
+
module AutoFulfill = AutomationSliceMaker.Make(AutoFulfillSpec, AutoFulfillAutomation)
|
|
175
173
|
|
|
176
174
|
// Mirrors the context that `Plugin_Builder` constructs for local deployments
|
|
177
175
|
// (environment from `LocalPluginSpec.environment`, platformName "local").
|
|
@@ -25,10 +25,7 @@ describe("MixedSource AutomationSlice", () => {
|
|
|
25
25
|
|
|
26
26
|
testPromise("Aggregate-source event creates a TODO and produces a command", async () => {
|
|
27
27
|
publishedCommands := []
|
|
28
|
-
let _ = await publishAggregateEvent(
|
|
29
|
-
"env-a1",
|
|
30
|
-
OrderShipped({orderId: "o1", productId: "p1"}),
|
|
31
|
-
)
|
|
28
|
+
let _ = await publishAggregateEvent("env-a1", OrderShipped({orderId: "o1", productId: "p1"}))
|
|
32
29
|
|
|
33
30
|
// Trigger phase2 manually since we don't have a heartbeat in this test.
|
|
34
31
|
let ops = await slice->ReventlessCore.Component.operations->TestRunner.resolve
|
|
@@ -52,10 +49,7 @@ describe("MixedSource AutomationSlice", () => {
|
|
|
52
49
|
|
|
53
50
|
testPromise("DCB-source StockReserved creates a TODO and produces a command", async () => {
|
|
54
51
|
publishedCommands := []
|
|
55
|
-
let _ = await publishDcbEvent(
|
|
56
|
-
"env-d1",
|
|
57
|
-
StockReserved({orderId: "o2", productId: "p2"}),
|
|
58
|
-
)
|
|
52
|
+
let _ = await publishDcbEvent("env-d1", StockReserved({orderId: "o2", productId: "p2"}))
|
|
59
53
|
|
|
60
54
|
let ops = await slice->ReventlessCore.Component.operations->TestRunner.resolve
|
|
61
55
|
await ops.processPending()
|
|
@@ -70,14 +64,8 @@ describe("MixedSource AutomationSlice", () => {
|
|
|
70
64
|
// Reserve fires phase1+phase2 (inline) — produces 1 command, item becomes
|
|
71
65
|
// Processing. StockReleased then resolves the item to Completed —
|
|
72
66
|
// subsequent processPending calls produce no further commands for it.
|
|
73
|
-
let _ = await publishDcbEvent(
|
|
74
|
-
|
|
75
|
-
StockReserved({orderId: "o3", productId: "p3"}),
|
|
76
|
-
)
|
|
77
|
-
let _ = await publishDcbEvent(
|
|
78
|
-
"env-d3",
|
|
79
|
-
StockReleased({orderId: "o3", productId: "p3"}),
|
|
80
|
-
)
|
|
67
|
+
let _ = await publishDcbEvent("env-d2", StockReserved({orderId: "o3", productId: "p3"}))
|
|
68
|
+
let _ = await publishDcbEvent("env-d3", StockReleased({orderId: "o3", productId: "p3"}))
|
|
81
69
|
|
|
82
70
|
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
83
71
|
|
|
@@ -91,14 +79,8 @@ describe("MixedSource AutomationSlice", () => {
|
|
|
91
79
|
|
|
92
80
|
testPromise("both sources can drive items into the same TODO list independently", async () => {
|
|
93
81
|
publishedCommands := []
|
|
94
|
-
let _ = await publishAggregateEvent(
|
|
95
|
-
|
|
96
|
-
OrderShipped({orderId: "o4", productId: "p4"}),
|
|
97
|
-
)
|
|
98
|
-
let _ = await publishDcbEvent(
|
|
99
|
-
"env-d4",
|
|
100
|
-
StockReserved({orderId: "o5", productId: "p5"}),
|
|
101
|
-
)
|
|
82
|
+
let _ = await publishAggregateEvent("env-a2", OrderShipped({orderId: "o4", productId: "p4"}))
|
|
83
|
+
let _ = await publishDcbEvent("env-d4", StockReserved({orderId: "o5", productId: "p5"}))
|
|
102
84
|
|
|
103
85
|
let ops = await slice->ReventlessCore.Component.operations->TestRunner.resolve
|
|
104
86
|
await ops.processPending()
|
|
@@ -112,14 +94,8 @@ describe("MixedSource AutomationSlice", () => {
|
|
|
112
94
|
testPromise("first-writer-wins idempotency across mappings sharing an ID", async () => {
|
|
113
95
|
publishedCommands := []
|
|
114
96
|
// Both sources produce the same composite ID — second arrival is ignored.
|
|
115
|
-
let _ = await publishAggregateEvent(
|
|
116
|
-
|
|
117
|
-
OrderShipped({orderId: "o6", productId: "p6"}),
|
|
118
|
-
)
|
|
119
|
-
let _ = await publishDcbEvent(
|
|
120
|
-
"env-d5",
|
|
121
|
-
StockReserved({orderId: "o6", productId: "p6"}),
|
|
122
|
-
)
|
|
97
|
+
let _ = await publishAggregateEvent("env-a3", OrderShipped({orderId: "o6", productId: "p6"}))
|
|
98
|
+
let _ = await publishDcbEvent("env-d5", StockReserved({orderId: "o6", productId: "p6"}))
|
|
123
99
|
|
|
124
100
|
let ops = await slice->ReventlessCore.Component.operations->TestRunner.resolve
|
|
125
101
|
await ops.processPending()
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Fixtures for CommandGenerator integration tests.
|
|
2
2
|
// Verifies that makeHandler builds a resolver that publishes the correct commandJson.
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
// Activate Pulumi mock mode (must be called before any Component.make)
|
|
6
5
|
let _ = TestRunner.setup()
|
|
7
6
|
|
|
@@ -14,13 +13,13 @@ module CGSpec = {
|
|
|
14
13
|
let name = "TestCGAggregate"
|
|
15
14
|
|
|
16
15
|
@schema
|
|
17
|
-
type command =
|
|
16
|
+
type command = CreateCGItem({name: string})
|
|
18
17
|
|
|
19
18
|
@schema
|
|
20
|
-
type event =
|
|
19
|
+
type event = CGItemCreated({name: string})
|
|
21
20
|
|
|
22
21
|
@schema
|
|
23
|
-
type error =
|
|
22
|
+
type error = CGAlreadyExists
|
|
24
23
|
|
|
25
24
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
26
25
|
}
|
|
@@ -42,7 +41,10 @@ module CGBehavior = {
|
|
|
42
41
|
| CGItemCreated({name}) => {name: name}
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
let decide = (_state: state, command: CGSpec.command): result<
|
|
44
|
+
let decide = (_state: state, command: CGSpec.command): result<
|
|
45
|
+
array<CGSpec.event>,
|
|
46
|
+
CGSpec.error,
|
|
47
|
+
> =>
|
|
46
48
|
switch command {
|
|
47
49
|
| CreateCGItem({name}) => Ok([CGSpec.CGItemCreated({name: name})])
|
|
48
50
|
}
|
|
@@ -8,36 +8,45 @@ describe("CommandGenerator_Builder.Make:", () => {
|
|
|
8
8
|
let _ = beforeEach(() => resetMocks())
|
|
9
9
|
|
|
10
10
|
describe("makeHandler:", () => {
|
|
11
|
-
testPromise(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
11
|
+
testPromise(
|
|
12
|
+
"CreateCGItem payload publishes correct commandJson",
|
|
13
|
+
async () => {
|
|
14
|
+
// Resolve the handler from the Output wrapper
|
|
15
|
+
let handler = await CGMaker.makeHandler(
|
|
16
|
+
~publishJsons=mockPublish,
|
|
17
|
+
~publishJsonsAndWait=None,
|
|
18
|
+
)->TestRunner.resolve
|
|
19
|
+
|
|
20
|
+
// Build payload: arguments must include id AND the command fields (name).
|
|
21
|
+
// generateCommand stringifies arguments, slices off id (index 0), uses the rest as params.
|
|
22
|
+
let payload: ReventlessCore.CommandGenerator.payload = {
|
|
23
|
+
command: "CreateCGItem",
|
|
24
|
+
arguments: {"id": "item-1", "name": "widget"}->Obj.magic,
|
|
25
|
+
meta: {ip: ["127.0.0.1"], user: "testuser", info: ""},
|
|
26
|
+
identity: Reventless.Identity.anonymous,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Call handler — generateCommand is async (awaits publishJsons internally)
|
|
30
|
+
let _ = await handler(payload, ())->Effect.runPromise
|
|
31
|
+
|
|
32
|
+
expect(capturedCmds.contents->Array.length)->toBe(1)
|
|
33
|
+
|
|
34
|
+
let cmd = capturedCmds.contents->Array.getUnsafe(0)
|
|
35
|
+
expect(cmd.id)->toBe("item-1")
|
|
36
|
+
|
|
37
|
+
// commandJson should decode to CreateCGItem({name: "widget"})
|
|
38
|
+
let decoded = cmd.commandJson->Reventless.Util_Sury.fromJson(CGSpec.commandSchema)
|
|
39
|
+
expect(decoded)->toEqual(CGSpec.CreateCGItem({name: "widget"}))
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
testPromise(
|
|
44
|
+
"make creates a CommandGenerator component without throwing",
|
|
45
|
+
async () => {
|
|
46
|
+
let cg = CGMaker.make(~name="test-cg")
|
|
47
|
+
let _ = cg
|
|
48
|
+
expect(true)->toBe(true)
|
|
49
|
+
},
|
|
50
|
+
)
|
|
42
51
|
})
|
|
43
52
|
})
|
|
@@ -5,13 +5,13 @@ module ItemSpec = {
|
|
|
5
5
|
let name = "TestCommandTopicItem"
|
|
6
6
|
|
|
7
7
|
@schema
|
|
8
|
-
type command =
|
|
8
|
+
type command = CreateItem({name: string}) | DeleteItem({id: string})
|
|
9
9
|
|
|
10
10
|
@schema
|
|
11
|
-
type event =
|
|
11
|
+
type event = ItemCreated({name: string})
|
|
12
12
|
|
|
13
13
|
@schema
|
|
14
|
-
type error =
|
|
14
|
+
type error = AlreadyExists
|
|
15
15
|
|
|
16
16
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
17
17
|
}
|
|
@@ -34,4 +34,3 @@ module CommandTopicMaker = ReventlessCore.CommandTopic_Builder.Make(
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
let cmdTopic = CommandTopicMaker.make(~name="TestCommandTopic")
|
|
37
|
-
|
|
@@ -7,13 +7,13 @@ module ItemSpec = {
|
|
|
7
7
|
let name = "StreamCmdItem"
|
|
8
8
|
|
|
9
9
|
@schema
|
|
10
|
-
type command =
|
|
10
|
+
type command = CreateItem({name: string}) | DeleteItem({id: string})
|
|
11
11
|
|
|
12
12
|
@schema
|
|
13
|
-
type event =
|
|
13
|
+
type event = ItemCreated({name: string})
|
|
14
14
|
|
|
15
15
|
@schema
|
|
16
|
-
type error =
|
|
16
|
+
type error = AlreadyExists
|
|
17
17
|
|
|
18
18
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
19
19
|
}
|
|
@@ -37,7 +37,9 @@ let _ = StreamBus.registerCommandHandler("StreamCmdTopicCmdTopic", async (body,
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
let makeJson = (id): Reventless.Message.commandJson => {
|
|
40
|
-
Reventless.Message.id
|
|
40
|
+
Reventless.Message.id,
|
|
41
41
|
meta: testMeta,
|
|
42
|
-
commandJson: ItemSpec.CreateItem({name: id})->ReventlessCore.Message.encode(
|
|
42
|
+
commandJson: ItemSpec.CreateItem({name: id})->ReventlessCore.Message.encode(
|
|
43
|
+
ItemSpec.commandSchema,
|
|
44
|
+
),
|
|
43
45
|
}
|
|
@@ -11,14 +11,17 @@ describe("CommandTopic (in-memory)", () => {
|
|
|
11
11
|
|
|
12
12
|
testPromise("publishJsons succeeds without throwing", async () => {
|
|
13
13
|
let ops = await cmdTopic->ReventlessCore.Component.operations->TestRunner.resolve
|
|
14
|
-
let commandJson =
|
|
14
|
+
let commandJson =
|
|
15
|
+
ItemSpec.CreateItem({name: "Widget"})->ReventlessCore.Message.encode(ItemSpec.commandSchema)
|
|
15
16
|
let didThrow = ref(false)
|
|
16
17
|
try {
|
|
17
|
-
await ops.publishJsons([
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
await ops.publishJsons([
|
|
19
|
+
{
|
|
20
|
+
Reventless.Message.id: "item-1",
|
|
21
|
+
meta: testMeta,
|
|
22
|
+
commandJson,
|
|
23
|
+
},
|
|
24
|
+
])
|
|
22
25
|
} catch {
|
|
23
26
|
| _ => didThrow := true
|
|
24
27
|
}
|
|
@@ -41,15 +44,21 @@ describe("CommandTopic (in-memory)", () => {
|
|
|
41
44
|
// Verify the bus receives the command on the expected channel.
|
|
42
45
|
// Channel name = make name ++ ComponentType.toName(CommandTopic) = "TestCommandTopic" ++ "CmdTopic"
|
|
43
46
|
let received: ref<bool> = ref(false)
|
|
44
|
-
Bus.registerCommandHandler(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
Bus.registerCommandHandler(
|
|
48
|
+
"TestCommandTopicCmdTopic",
|
|
49
|
+
async (_json, _ctx) => {
|
|
50
|
+
received := true
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
let commandJson =
|
|
54
|
+
ItemSpec.DeleteItem({id: "item-99"})->ReventlessCore.Message.encode(ItemSpec.commandSchema)
|
|
55
|
+
await ops.publishJsons([
|
|
56
|
+
{
|
|
57
|
+
Reventless.Message.id: "item-99",
|
|
58
|
+
meta: testMeta,
|
|
59
|
+
commandJson,
|
|
60
|
+
},
|
|
61
|
+
])
|
|
53
62
|
expect(received.contents)->toBe(true)
|
|
54
63
|
})
|
|
55
64
|
})
|
|
@@ -23,14 +23,12 @@ module CounterMaker = Counter_Builder.Make(Bus)
|
|
|
23
23
|
|
|
24
24
|
let counterEvents: ref<array<JSON.t>> = ref([])
|
|
25
25
|
|
|
26
|
-
let counter = CounterMaker.make(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
->
|
|
31
|
-
|
|
32
|
-
counterEvents := counterEvents.contents->Array.concat(chunk)
|
|
33
|
-
}),
|
|
26
|
+
let counter = CounterMaker.make(~name="TestCounter", ~jsonEventsHandler=stream =>
|
|
27
|
+
stream
|
|
28
|
+
->Stream.runCollect
|
|
29
|
+
->Effect.map(chunk => {
|
|
30
|
+
counterEvents := counterEvents.contents->Array.concat(chunk)
|
|
31
|
+
})
|
|
34
32
|
)
|
|
35
33
|
|
|
36
34
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -69,7 +69,10 @@ module AddProductBehavior = {
|
|
|
69
69
|
let evolve = (state: state, event: Spec.consumedEvent): state =>
|
|
70
70
|
switch event {
|
|
71
71
|
| ProductAdded(_) => {...state, exists: true}
|
|
72
|
-
| CategoryAdded({categoryId}) => {
|
|
72
|
+
| CategoryAdded({categoryId}) => {
|
|
73
|
+
...state,
|
|
74
|
+
liveCats: Array.concat(state.liveCats, [categoryId]),
|
|
75
|
+
}
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
let decide = (state: state, command: Spec.command): result<array<Spec.event>, Spec.error> =>
|
|
@@ -148,7 +151,10 @@ let publishJsonsOutput = publishJsons->Pulumi.Output.make
|
|
|
148
151
|
let _addProductSlice = AddProductMaker.make(
|
|
149
152
|
~dcbEventLog=eventLog,
|
|
150
153
|
~publishJsons=publishJsonsOutput,
|
|
151
|
-
~tagKeysByEventType=Dict.fromArray([
|
|
154
|
+
~tagKeysByEventType=Dict.fromArray([
|
|
155
|
+
("ProductAdded", ["productId"]),
|
|
156
|
+
("CategoryAdded", ["categoryId"]),
|
|
157
|
+
]),
|
|
152
158
|
~crossPartitionTagKeys=["categoryId"],
|
|
153
159
|
)
|
|
154
160
|
|
|
@@ -44,25 +44,32 @@ describe("DCB inferred cross-partition read (sibling exclusion)", () => {
|
|
|
44
44
|
await dispatch(addProductJson("p3", "cat3", "Blocks"), "p3")
|
|
45
45
|
|
|
46
46
|
// Queryable by its own partition key…
|
|
47
|
-
let byProduct = await ops.read(
|
|
47
|
+
let byProduct = await ops.read(
|
|
48
|
+
~query=[{tags: [{Reventless.DcbTag.key: "productId", value: "p3"}]}],
|
|
49
|
+
)
|
|
48
50
|
expect(byProduct.events->Array.some(e => e.eventType == "ProductAdded"))->toBe(true)
|
|
49
51
|
|
|
50
52
|
// …but the categoryId index returns only the category's own event, never p3.
|
|
51
|
-
let byCategory = await ops.read(
|
|
53
|
+
let byCategory = await ops.read(
|
|
54
|
+
~query=[{tags: [{Reventless.DcbTag.key: "categoryId", value: "cat3"}]}],
|
|
55
|
+
)
|
|
52
56
|
expect(byCategory.events->Array.some(e => e.eventType == "ProductAdded"))->toBe(false)
|
|
53
57
|
expect(byCategory.events->Array.some(e => e.eventType == "CategoryAdded"))->toBe(true)
|
|
54
58
|
},
|
|
55
59
|
)
|
|
56
60
|
|
|
57
|
-
testPromise(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
testPromise(
|
|
62
|
+
"re-adding the same product is still rejected (its own read is partition-scoped)",
|
|
63
|
+
async () => {
|
|
64
|
+
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
65
|
+
let _ = await ops.append([
|
|
66
|
+
encodeEvent(CategoryAdded({categoryId: "cat2", name: "Books"})),
|
|
67
|
+
encodeEvent(ProductAdded({productId: "p9", categoryId: "cat2", name: "Novel"})),
|
|
68
|
+
])
|
|
69
|
+
capturedEventCount := 0 // count only events produced by the command below
|
|
70
|
+
// p9 already exists — its own productId clause returns its ProductAdded.
|
|
71
|
+
await dispatch(addProductJson("p9", "cat2", "Novel"), "p9")
|
|
72
|
+
expect(capturedEventCount.contents)->toBe(0)
|
|
73
|
+
},
|
|
74
|
+
)
|
|
68
75
|
})
|
|
@@ -25,10 +25,11 @@ describe("DcbEventLog.appendStream (in-memory adapter)", () => {
|
|
|
25
25
|
|
|
26
26
|
testPromise("appendStream writes all events from stream", async () => {
|
|
27
27
|
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
28
|
-
let stream =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
let stream =
|
|
29
|
+
[
|
|
30
|
+
encodeEvent(DcbFixtures.ItemEventLog.ItemAdded({id: "as-w1", name: "Widget1"})),
|
|
31
|
+
encodeEvent(DcbFixtures.ItemEventLog.ItemAdded({id: "as-w1", name: "Widget2"})),
|
|
32
|
+
]->Stream.fromIterable
|
|
32
33
|
let result = await ops.appendStream(stream)->Effect.runPromise
|
|
33
34
|
let isOk = switch result {
|
|
34
35
|
| Ok(_) => true
|
|
@@ -52,10 +53,12 @@ describe("DcbEventLog.appendStream (in-memory adapter)", () => {
|
|
|
52
53
|
|
|
53
54
|
testPromise("readStream → appendStream pipeline copies events", async () => {
|
|
54
55
|
let ops = await eventLog->ReventlessCore.Component.operations->TestRunner.resolve
|
|
55
|
-
let _ = await ops.append([
|
|
56
|
+
let _ = await ops.append([
|
|
57
|
+
encodeEvent(DcbFixtures.ItemEventLog.ItemAdded({id: "as-src", name: "Source"})),
|
|
58
|
+
])
|
|
56
59
|
// readStream returns rawSequencedEvent; extract bare rawEvent for appendStream
|
|
57
|
-
let srcStream =
|
|
58
|
-
|
|
60
|
+
let srcStream = ops.readStream(~query=tagQuery("as-src"))->Stream.map(
|
|
61
|
+
se => {
|
|
59
62
|
let rawEvent: ReventlessInfra.DcbEventLog.rawEvent = {
|
|
60
63
|
eventType: se.eventType,
|
|
61
64
|
data: se.data,
|
|
@@ -63,7 +66,8 @@ describe("DcbEventLog.appendStream (in-memory adapter)", () => {
|
|
|
63
66
|
meta: se.meta,
|
|
64
67
|
}
|
|
65
68
|
rawEvent
|
|
66
|
-
}
|
|
69
|
+
},
|
|
70
|
+
)
|
|
67
71
|
let _ = await ops.appendStream(srcStream)->Effect.runPromise
|
|
68
72
|
let dst = await ops.read(~query=tagQuery("as-src"))
|
|
69
73
|
// 1 original + 1 copy = 2 events for the same tag
|