@reventlessdev/reventless-core 3.0.0-alpha.259 → 3.0.0-alpha.261
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 +15 -0
- package/package.json +11 -11
- package/src/Behavior.res +1 -4
- package/src/Message.res +17 -14
- package/src/Projection.res +51 -25
- package/src/RequestContext.res +8 -9
- package/src/ResourceAttribution.res +28 -14
- package/src/adapter/Adapter.res +48 -4
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
- package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
- package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
- package/src/adapter/Monitoring/Monitoring.res +16 -17
- package/src/adapter/QueryInterception/QueryInterception.res +0 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
- package/src/admin/Platform_Admin.res +17 -6
- package/src/admin/Platform_AdminApi.res +14 -9
- package/src/admin/Platform_Admin_Callback.res +1 -1
- package/src/admin/Platform_BakedManifest.res +34 -34
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
- package/src/admin/Platform_PluginStructuresApi.res +15 -12
- package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate.res +13 -12
- package/src/components/Aggregate/Aggregate_Builder.res +22 -21
- package/src/components/Aggregate/Aggregate_Callback.res +91 -86
- package/src/components/Api/ApiNoApiHelpers.res +1 -3
- package/src/components/Api/Api_Ids.res +0 -1
- package/src/components/Api/Api_Naming.res +41 -18
- package/src/components/Api/GraphQL_FragmentGenerator.res +42 -52
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +11 -4
- package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
- package/src/components/Api/GraphQL_Stitcher.res +9 -10
- package/src/components/Api/MCP_SchemaGenerator.res +8 -12
- package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
- package/src/components/Api/QueryDbListQuery.res +13 -6
- package/src/components/Api/SchemaType.res +40 -20
- package/src/components/Api/SchemaType.res.mjs +16 -0
- package/src/components/Api/SuryToJsonSchema.res +18 -10
- package/src/components/Api/SuryToJsonSchema.res.mjs +135 -121
- package/src/components/AutomationSlice/AutomationSlice.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
- package/src/components/Builder_Helpers.res +12 -23
- package/src/components/CommandGenerator/CommandGenerator.res +6 -1
- package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
- package/src/components/CommandTopic/CommandTopic.res +3 -3
- package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
- package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
- package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
- package/src/components/Counter/Counter_Builder.res +1 -6
- package/src/components/Counter/Counter_Callback.res +3 -3
- package/src/components/Dcb/Dcb_Builder.res +970 -956
- package/src/components/DcbEventLog/DcbEventLog.res +4 -4
- package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
- package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
- package/src/components/EventCollector/EventCollector_Builder.res +2 -1
- package/src/components/EventLog/EventLog.res +5 -1
- package/src/components/EventLog/EventLog_Operations.res +12 -8
- package/src/components/EventMapper/EventMapper.res +8 -10
- package/src/components/EventMapper/EventMapper_Builder.res +4 -1
- package/src/components/EventMapper/EventMapper_Callback.res +46 -31
- package/src/components/EventTopic/EventTopic.res +3 -3
- package/src/components/EventTopic/EventTopic_Builder.res +1 -1
- package/src/components/Extension/Extension.res +4 -1
- package/src/components/Extension/Extension_Operations.res +11 -3
- package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
- package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
- package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
- package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
- package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
- package/src/components/QueryDb/QueryDb_Operations.res +10 -10
- package/src/components/ReadModel/ReadModel.res +3 -3
- package/src/components/ReadModel/ReadModel_Builder.res +1 -2
- package/src/components/ReadModel/ReadModel_Callback.res +2 -1
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
- package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
- package/src/components/StateTopic.res +1 -3
- package/src/components/StateViewSlice/StateViewSlice.res +4 -4
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
- package/src/components/Task/Task.res +3 -3
- package/src/components/Task/Task_Adapter.res +4 -1
- package/src/components/Task/Task_Builder.res +24 -21
- package/src/plugin/api/PluginBaseFragment.res +9 -3
- package/src/plugin/component/Capability_Inference.res +8 -10
- package/src/plugin/component/Plugin.res +6 -6
- package/src/plugin/component/Plugin_Builder.res +54 -50
- package/src/plugin/component/Plugin_Callback.res +10 -6
- package/src/plugin/component/Plugin_Helpers.res +264 -230
- package/src/plugin/component/Plugin_Structure.res +370 -324
- package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
- package/src/plugin/component/SchemaWalker.res +11 -12
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
- package/src/plugin/lifecycle/PluginBehavior.res +3 -4
- package/src/plugin/lifecycle/PluginSpec.res +1 -2
- package/src/plugin/lifecycle/PluginsProjection.res +16 -5
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
- package/src/util/CommandPublisher.res +9 -2
- package/src/util/CsvStream.res +3 -3
- package/src/util/Interstack.res +7 -15
- package/src/util/Interstack.resi +3 -3
- package/src/util/LogFormat.res +5 -9
- package/src/util/Logger.res +1 -2
- package/src/util/Lru.res +2 -1
- package/src/util/Lru.resi +2 -3
- package/src/util/ScheduleOps.res +24 -27
- package/src/util/StorePrefixCollision.res +6 -9
- package/src/util/Util_Promise.res +4 -1
- package/src/util/Util_Promise.res.mjs +1 -1
- package/tests/IdentityTest.res +166 -113
- package/tests/ResourceAttributionTest.res +28 -25
- package/tests/StorePrefixCollisionTest.res +2 -1
- package/tests/TaskBucketNamingTest.res +3 -2
- package/tests/adapter/EventLogProvisioningTest.res +51 -48
- package/tests/adapter/MonitoringTest.res +81 -38
- package/tests/adapter/QueryInterceptionTest.res +2 -2
- package/tests/adapter/RefusalVocabularyTest.res +31 -23
- package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
- package/tests/adapter/RuntimeExtensionTest.res +5 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
- package/tests/admin/Platform_Admin_StructureTest.res +10 -11
- package/tests/admin/Platform_BakedManifestTest.res +31 -35
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
- package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
- package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
- package/tests/aggregate/AggregateCacheTest.res +10 -12
- package/tests/aggregate/AggregateCallbackTest.res +75 -46
- package/tests/aggregate/AggregateCausationTest.res +19 -20
- package/tests/aggregate/AggregateConflictTest.res +41 -19
- package/tests/aggregate/AggregateFixtures.res +15 -5
- package/tests/aggregate/AggregateRejectionTest.res +97 -80
- package/tests/aggregate/AggregateSnapshotTest.res +44 -16
- package/tests/api/Api_IdsTest.res +10 -4
- package/tests/api/BrandedMarkerCompositionTest.res +48 -36
- package/tests/api/GeolocationSemanticTest.res +3 -3
- package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
- package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
- package/tests/api/StorageRefFieldsTest.res +30 -18
- package/tests/api/SuryToJsonSchemaTest.res +1338 -981
- package/tests/api/SuryToJsonSchemaTest.res.mjs +31 -0
- package/tests/api/TaggedUnionSchemaTest.res +17 -24
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
- package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
- package/tests/commandgenerator/OwnerStampingTest.res +91 -76
- package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
- package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
- package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
- package/tests/counter/CounterCallbackTest.res +40 -34
- package/tests/counter/CounterFixtures.res +4 -4
- package/tests/dcb/DcbDecodeTest.res +115 -84
- package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
- package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
- package/tests/dcb/DcbFixtures.res +4 -5
- package/tests/dcb/DcbScopeInferenceTest.res +173 -114
- package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
- package/tests/dcb/DcbTagTest.res +22 -11
- package/tests/dcb/DcbValidationTest.res +288 -218
- package/tests/eventlog/EventLogFixtures.res +4 -4
- package/tests/eventlog/EventLogOperationsTest.res +102 -67
- package/tests/eventlog/EventLogRetryTest.res +104 -65
- package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
- package/tests/eventmapper/EventMapperFixtures.res +15 -23
- package/tests/eventmapper/EventMapperTest.res +54 -38
- package/tests/eventtopic/EventTopicFixtures.res +3 -4
- package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
- package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
- package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
- package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
- package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
- package/tests/logger/LogFormatTest.res +104 -82
- package/tests/logger/LoggerLazyTest.res +21 -12
- package/tests/message/MessageTest.res +96 -93
- package/tests/message/MetaEnvelopeTest.res +26 -30
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
- package/tests/plugin/ManifestVisibilityTest.res +20 -12
- package/tests/plugin/PluginDefinitionScalars.res +1 -2
- package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
- package/tests/plugin/PluginSpecExperiment.res +0 -1
- package/tests/plugin/PluginStructureTest.res +1533 -1030
- package/tests/plugin/PluginStructureTest.res.mjs +6 -0
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
- package/tests/plugin/SchemaWalkerTest.res +116 -70
- package/tests/plugin/StateChange/PsReserveStock.res +4 -4
- package/tests/plugin/StateView/PsAnnotatedView.res +6 -4
- package/tests/plugin/StateView/PsAnnotatedView.res.mjs +7 -3
- package/tests/plugin/StateView/PsCustomersView.res +4 -4
- package/tests/plugin/StateView/PsShipmentsView.res +3 -2
- package/tests/projection/LifecycleTrailTest.res +1 -4
- package/tests/projection/ProjectionOptimizeTest.res +13 -9
- package/tests/querydb/QueryDbFixtures.res +0 -1
- package/tests/querydb/QueryDbOperationsTest.res +99 -64
- package/tests/querydb/TaggedUnionStampTest.res +10 -8
- package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
- package/tests/util/CommandPublisherTest.res +0 -1
- package/tests/util/CsvStreamTest.res +3 -3
- package/tests/util/LruTest.res +84 -56
|
@@ -66,298 +66,427 @@ let _ = beforeEach(() => {
|
|
|
66
66
|
|
|
67
67
|
describe("StateChangeSlice_Callback:", () => {
|
|
68
68
|
describe("handleCommands - happy path", () => {
|
|
69
|
-
testPromise(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
69
|
+
testPromise(
|
|
70
|
+
"CreateItem on empty log succeeds",
|
|
71
|
+
async () => {
|
|
72
|
+
let results = await TestHandler.handleCommands(
|
|
73
|
+
testDcbEventLog,
|
|
74
|
+
Stream.fromIterable([
|
|
75
|
+
makeTopicItem(
|
|
76
|
+
"ref-1",
|
|
77
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
78
|
+
),
|
|
79
|
+
]),
|
|
80
|
+
)->Effect.runPromise
|
|
81
|
+
|
|
82
|
+
expect((
|
|
83
|
+
results,
|
|
84
|
+
mock.getEvents()->Array.length,
|
|
85
|
+
mock.publishedEvents.contents->Array.length,
|
|
86
|
+
))->toEqual(([Ok("ref-1")], 1, 1))
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
testPromise(
|
|
91
|
+
"stored event carries only its entity tags (no framework provenance tag)",
|
|
92
|
+
async () => {
|
|
93
|
+
let _ = await TestHandler.handleCommands(
|
|
94
|
+
testDcbEventLog,
|
|
95
|
+
Stream.fromIterable([
|
|
96
|
+
makeTopicItem(
|
|
97
|
+
"ref-1",
|
|
98
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
99
|
+
),
|
|
100
|
+
]),
|
|
101
|
+
)->Effect.runPromise
|
|
102
|
+
let storedEvent = mock.getEvents()->Array.getUnsafe(0)
|
|
103
|
+
|
|
104
|
+
// No `originatorSlice` (or any other) metadata tag is smuggled into the tag
|
|
105
|
+
// set — a stored event's tags are exactly its DCB entity tags, so the
|
|
106
|
+
// composite read/write keys stay aligned. See
|
|
107
|
+
// docs/analysis/dcb-event-provenance-and-metadata.md.
|
|
108
|
+
expect(storedEvent.tags)->toEqual([{Reventless.DcbTag.key: "itemId", value: "item-1"}])
|
|
109
|
+
},
|
|
110
|
+
)
|
|
99
111
|
})
|
|
100
112
|
|
|
101
113
|
describe("handleCommands - decide returns Ok([])", () => {
|
|
102
|
-
testPromise(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
testPromise(
|
|
115
|
+
"NoOp returns Ok without storing events",
|
|
116
|
+
async () => {
|
|
117
|
+
let results = await TestHandler.handleCommands(
|
|
118
|
+
testDcbEventLog,
|
|
119
|
+
Stream.fromIterable([makeTopicItem("ref-noop", DcbFixtures.TestCommandSpec.NoOp)]),
|
|
120
|
+
)->Effect.runPromise
|
|
121
|
+
|
|
122
|
+
expect((results, mock.getEvents()->Array.length))->toEqual(([Ok("ref-noop")], 0))
|
|
123
|
+
},
|
|
124
|
+
)
|
|
110
125
|
})
|
|
111
126
|
|
|
112
127
|
describe("handleCommands - decide returns Error", () => {
|
|
113
|
-
testPromise(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
testPromise(
|
|
129
|
+
"CreateItem when item exists returns Ok (business-rule violations ACK, not NACK)",
|
|
130
|
+
async () => {
|
|
131
|
+
// First create the item
|
|
132
|
+
let _ = await TestHandler.handleCommands(
|
|
133
|
+
testDcbEventLog,
|
|
134
|
+
Stream.fromIterable([
|
|
135
|
+
makeTopicItem(
|
|
136
|
+
"ref-1",
|
|
137
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
138
|
+
),
|
|
139
|
+
]),
|
|
140
|
+
)->Effect.runPromise
|
|
141
|
+
// Try to create again — decide returns Error, but handler returns Ok("rejected") to avoid infinite SQS retry
|
|
142
|
+
let results = await TestHandler.handleCommands(
|
|
143
|
+
testDcbEventLog,
|
|
144
|
+
Stream.fromIterable([
|
|
145
|
+
makeTopicItem(
|
|
146
|
+
"ref-2",
|
|
147
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Duplicate"}),
|
|
148
|
+
),
|
|
149
|
+
]),
|
|
150
|
+
)->Effect.runPromise
|
|
151
|
+
|
|
152
|
+
expect(results)->toEqual([Ok("ref-2")])
|
|
153
|
+
},
|
|
154
|
+
)
|
|
131
155
|
})
|
|
132
156
|
|
|
133
157
|
describe("handleCommands - retry on conflict", () => {
|
|
134
|
-
testPromise(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
testPromise(
|
|
159
|
+
"retries and succeeds after 1 append failure",
|
|
160
|
+
async () => {
|
|
161
|
+
mock.failNextAppends := 1
|
|
162
|
+
let results = await TestHandler.handleCommands(
|
|
163
|
+
testDcbEventLog,
|
|
164
|
+
Stream.fromIterable([
|
|
165
|
+
makeTopicItem(
|
|
166
|
+
"ref-1",
|
|
167
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
168
|
+
),
|
|
169
|
+
]),
|
|
170
|
+
)->Effect.runPromise
|
|
171
|
+
|
|
172
|
+
expect((results, mock.getEvents()->Array.length))->toEqual(([Ok("ref-1")], 1))
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
testPromise(
|
|
177
|
+
"returns Error after retries exhausted (4 failures)",
|
|
178
|
+
async () => {
|
|
179
|
+
mock.failNextAppends := 4
|
|
180
|
+
let results = await TestHandler.handleCommands(
|
|
181
|
+
testDcbEventLog,
|
|
182
|
+
Stream.fromIterable([
|
|
183
|
+
makeTopicItem(
|
|
184
|
+
"ref-1",
|
|
185
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
186
|
+
),
|
|
187
|
+
]),
|
|
188
|
+
)->Effect.runPromise
|
|
189
|
+
|
|
190
|
+
expect((results, mock.getEvents()->Array.length))->toEqual(([Error("ref-1")], 0))
|
|
191
|
+
},
|
|
192
|
+
)
|
|
157
193
|
})
|
|
158
194
|
|
|
159
195
|
describe("handleCommands - read consistency (eventual-first, strong-on-retry)", () => {
|
|
160
|
-
testPromise(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
testPromise(
|
|
197
|
+
"a conflict-free command reads eventually-consistent (no strong read)",
|
|
198
|
+
async () => {
|
|
199
|
+
let _ = await TestHandler.handleCommands(
|
|
200
|
+
testDcbEventLog,
|
|
201
|
+
Stream.fromIterable([
|
|
202
|
+
makeTopicItem(
|
|
203
|
+
"ref-1",
|
|
204
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
205
|
+
),
|
|
206
|
+
]),
|
|
207
|
+
)->Effect.runPromise
|
|
208
|
+
|
|
209
|
+
// Single attempt → single read, eventual.
|
|
210
|
+
expect(mock.readStrongConsistency.contents)->toEqual([false])
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
testPromise(
|
|
215
|
+
"escalates to strong consistency on retry after a conflict",
|
|
216
|
+
async () => {
|
|
217
|
+
mock.failNextAppends := 1
|
|
218
|
+
let _ = await TestHandler.handleCommands(
|
|
219
|
+
testDcbEventLog,
|
|
220
|
+
Stream.fromIterable([
|
|
221
|
+
makeTopicItem(
|
|
222
|
+
"ref-1",
|
|
223
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
224
|
+
),
|
|
225
|
+
]),
|
|
226
|
+
)->Effect.runPromise
|
|
227
|
+
|
|
228
|
+
// First attempt eventual, retry strong.
|
|
229
|
+
expect(mock.readStrongConsistency.contents)->toEqual([false, true])
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
testPromise(
|
|
234
|
+
"every retry after the first is strong (2 failures)",
|
|
235
|
+
async () => {
|
|
236
|
+
mock.failNextAppends := 2
|
|
237
|
+
let _ = await TestHandler.handleCommands(
|
|
238
|
+
testDcbEventLog,
|
|
239
|
+
Stream.fromIterable([
|
|
240
|
+
makeTopicItem(
|
|
241
|
+
"ref-1",
|
|
242
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
243
|
+
),
|
|
244
|
+
]),
|
|
245
|
+
)->Effect.runPromise
|
|
246
|
+
|
|
247
|
+
expect(mock.readStrongConsistency.contents)->toEqual([false, true, true])
|
|
248
|
+
},
|
|
249
|
+
)
|
|
196
250
|
})
|
|
197
251
|
|
|
198
252
|
describe("handleCommands - read consistency (per-slice override)", () => {
|
|
199
|
-
testPromise(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
253
|
+
testPromise(
|
|
254
|
+
"AlwaysStrong reads strong on the first attempt",
|
|
255
|
+
async () => {
|
|
256
|
+
let _ = await StrongHandler.handleCommands(
|
|
257
|
+
testDcbEventLog,
|
|
258
|
+
Stream.fromIterable([
|
|
259
|
+
makeTopicItem(
|
|
260
|
+
"ref-1",
|
|
261
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
262
|
+
),
|
|
263
|
+
]),
|
|
264
|
+
)->Effect.runPromise
|
|
265
|
+
|
|
266
|
+
// No escalation step — strong from the start.
|
|
267
|
+
expect(mock.readStrongConsistency.contents)->toEqual([true])
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
testPromise(
|
|
272
|
+
"AlwaysStrong stays strong across retries (2 failures)",
|
|
273
|
+
async () => {
|
|
274
|
+
mock.failNextAppends := 2
|
|
275
|
+
let _ = await StrongHandler.handleCommands(
|
|
276
|
+
testDcbEventLog,
|
|
277
|
+
Stream.fromIterable([
|
|
278
|
+
makeTopicItem(
|
|
279
|
+
"ref-1",
|
|
280
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
281
|
+
),
|
|
282
|
+
]),
|
|
283
|
+
)->Effect.runPromise
|
|
284
|
+
|
|
285
|
+
expect(mock.readStrongConsistency.contents)->toEqual([true, true, true])
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
testPromise(
|
|
290
|
+
"AlwaysEventual never reads strong, even on retry (2 failures)",
|
|
291
|
+
async () => {
|
|
292
|
+
mock.failNextAppends := 2
|
|
293
|
+
let _ = await EventualHandler.handleCommands(
|
|
294
|
+
testDcbEventLog,
|
|
295
|
+
Stream.fromIterable([
|
|
296
|
+
makeTopicItem(
|
|
297
|
+
"ref-1",
|
|
298
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
299
|
+
),
|
|
300
|
+
]),
|
|
301
|
+
)->Effect.runPromise
|
|
302
|
+
|
|
303
|
+
expect(mock.readStrongConsistency.contents)->toEqual([false, false, false])
|
|
304
|
+
},
|
|
305
|
+
)
|
|
234
306
|
})
|
|
235
307
|
|
|
236
308
|
describe("handleCommands - conditional append", () => {
|
|
237
|
-
testPromise(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
309
|
+
testPromise(
|
|
310
|
+
"RenameItem after CreateItem uses headPosition in condition",
|
|
311
|
+
async () => {
|
|
312
|
+
let _ = await TestHandler.handleCommands(
|
|
313
|
+
testDcbEventLog,
|
|
314
|
+
Stream.fromIterable([
|
|
315
|
+
makeTopicItem(
|
|
316
|
+
"ref-1",
|
|
317
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
|
|
318
|
+
),
|
|
319
|
+
]),
|
|
320
|
+
)->Effect.runPromise
|
|
321
|
+
let results = await TestHandler.handleCommands(
|
|
322
|
+
testDcbEventLog,
|
|
323
|
+
Stream.fromIterable([
|
|
324
|
+
makeTopicItem(
|
|
325
|
+
"ref-2",
|
|
326
|
+
DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "Updated"}),
|
|
327
|
+
),
|
|
328
|
+
]),
|
|
329
|
+
)->Effect.runPromise
|
|
257
330
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
testDcbEventLog,
|
|
262
|
-
Stream.fromIterable([
|
|
263
|
-
makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "First"})),
|
|
264
|
-
makeTopicItem("ref-2", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-2", name: "Second"})),
|
|
265
|
-
]),
|
|
266
|
-
)->Effect.runPromise
|
|
267
|
-
|
|
268
|
-
expect(results)->toEqual([Ok("ref-1"), Ok("ref-2")])
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
testPromise("mixed: new item succeeds, duplicate item ACKs as Ok (business-rule violation)", async () => {
|
|
272
|
-
// Create item-1 first
|
|
273
|
-
let _ = await TestHandler.handleCommands(
|
|
274
|
-
testDcbEventLog,
|
|
275
|
-
Stream.fromIterable([
|
|
276
|
-
makeTopicItem("ref-0", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Existing"})),
|
|
277
|
-
]),
|
|
278
|
-
)->Effect.runPromise
|
|
279
|
-
// Batch: create item-2 (ok) and duplicate item-1 (business-rule violation → Ok, not Error)
|
|
280
|
-
let results = await TestHandler.handleCommands(
|
|
281
|
-
testDcbEventLog,
|
|
282
|
-
Stream.fromIterable([
|
|
283
|
-
makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-2", name: "New"})),
|
|
284
|
-
makeTopicItem(
|
|
285
|
-
"ref-2",
|
|
286
|
-
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Duplicate"}),
|
|
287
|
-
),
|
|
288
|
-
]),
|
|
289
|
-
)->Effect.runPromise
|
|
290
|
-
|
|
291
|
-
expect(results)->toEqual([Ok("ref-1"), Ok("ref-2")])
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
testPromise("empty batch returns empty array", async () => {
|
|
295
|
-
let results = await TestHandler.handleCommands(
|
|
296
|
-
testDcbEventLog,
|
|
297
|
-
Stream.fromIterable([]),
|
|
298
|
-
)->Effect.runPromise
|
|
299
|
-
|
|
300
|
-
expect(results)->toEqual([])
|
|
301
|
-
})
|
|
331
|
+
expect((results, mock.getEvents()->Array.length))->toEqual(([Ok("ref-2")], 2))
|
|
332
|
+
},
|
|
333
|
+
)
|
|
302
334
|
})
|
|
303
335
|
|
|
304
|
-
describe("handleCommands -
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
testPromise("a third same-entity command reads only the delta", async () => {
|
|
310
|
-
let run = (reference, command) =>
|
|
311
|
-
TestHandler.handleCommands(
|
|
336
|
+
describe("handleCommands - batch", () => {
|
|
337
|
+
testPromise(
|
|
338
|
+
"multiple successful commands",
|
|
339
|
+
async () => {
|
|
340
|
+
let results = await TestHandler.handleCommands(
|
|
312
341
|
testDcbEventLog,
|
|
313
|
-
Stream.fromIterable([
|
|
342
|
+
Stream.fromIterable([
|
|
343
|
+
makeTopicItem(
|
|
344
|
+
"ref-1",
|
|
345
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "First"}),
|
|
346
|
+
),
|
|
347
|
+
makeTopicItem(
|
|
348
|
+
"ref-2",
|
|
349
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-2", name: "Second"}),
|
|
350
|
+
),
|
|
351
|
+
]),
|
|
314
352
|
)->Effect.runPromise
|
|
315
353
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
3,
|
|
326
|
-
))
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
testPromise("a different entity does not hit another entity's cache (full read)", async () => {
|
|
330
|
-
let run = (reference, command) =>
|
|
331
|
-
TestHandler.handleCommands(
|
|
354
|
+
expect(results)->toEqual([Ok("ref-1"), Ok("ref-2")])
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
testPromise(
|
|
359
|
+
"mixed: new item succeeds, duplicate item ACKs as Ok (business-rule violation)",
|
|
360
|
+
async () => {
|
|
361
|
+
// Create item-1 first
|
|
362
|
+
let _ = await TestHandler.handleCommands(
|
|
332
363
|
testDcbEventLog,
|
|
333
|
-
Stream.fromIterable([
|
|
364
|
+
Stream.fromIterable([
|
|
365
|
+
makeTopicItem(
|
|
366
|
+
"ref-0",
|
|
367
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Existing"}),
|
|
368
|
+
),
|
|
369
|
+
]),
|
|
370
|
+
)->Effect.runPromise
|
|
371
|
+
// Batch: create item-2 (ok) and duplicate item-1 (business-rule violation → Ok, not Error)
|
|
372
|
+
let results = await TestHandler.handleCommands(
|
|
373
|
+
testDcbEventLog,
|
|
374
|
+
Stream.fromIterable([
|
|
375
|
+
makeTopicItem(
|
|
376
|
+
"ref-1",
|
|
377
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-2", name: "New"}),
|
|
378
|
+
),
|
|
379
|
+
makeTopicItem(
|
|
380
|
+
"ref-2",
|
|
381
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Duplicate"}),
|
|
382
|
+
),
|
|
383
|
+
]),
|
|
334
384
|
)->Effect.runPromise
|
|
335
385
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// item-2's query is a distinct cache key → cold full read (after=None).
|
|
340
|
-
expect(mock.readAfters.contents)->toEqual([None, None])
|
|
341
|
-
})
|
|
386
|
+
expect(results)->toEqual([Ok("ref-1"), Ok("ref-2")])
|
|
387
|
+
},
|
|
388
|
+
)
|
|
342
389
|
|
|
343
|
-
testPromise(
|
|
344
|
-
|
|
345
|
-
|
|
390
|
+
testPromise(
|
|
391
|
+
"empty batch returns empty array",
|
|
392
|
+
async () => {
|
|
393
|
+
let results = await TestHandler.handleCommands(
|
|
346
394
|
testDcbEventLog,
|
|
347
|
-
Stream.fromIterable([
|
|
395
|
+
Stream.fromIterable([]),
|
|
348
396
|
)->Effect.runPromise
|
|
349
397
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
398
|
+
expect(results)->toEqual([])
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
describe("handleCommands - projection cache", () => {
|
|
404
|
+
// The decision-model projection cache (StateChangeSlice_Callback) caches the
|
|
405
|
+
// `(decisionState, readHead)` per query. A warm same-entity command then reads
|
|
406
|
+
// only events after `readHead` — observable here via `mock.readAfters`
|
|
407
|
+
// (Some(_) = delta read, None = full read).
|
|
408
|
+
testPromise(
|
|
409
|
+
"a third same-entity command reads only the delta",
|
|
410
|
+
async () => {
|
|
411
|
+
let run = (reference, command) =>
|
|
412
|
+
TestHandler.handleCommands(
|
|
413
|
+
testDcbEventLog,
|
|
414
|
+
Stream.fromIterable([makeTopicItem(reference, command)]),
|
|
415
|
+
)->Effect.runPromise
|
|
416
|
+
|
|
417
|
+
let _ = await run(
|
|
418
|
+
"ref-1",
|
|
419
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "A"}),
|
|
420
|
+
)
|
|
421
|
+
let _ = await run(
|
|
422
|
+
"ref-2",
|
|
423
|
+
DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "B"}),
|
|
424
|
+
)
|
|
425
|
+
let r3 = await run(
|
|
426
|
+
"ref-3",
|
|
427
|
+
DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "C"}),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
// Read 1 (create, empty log) and read 2 (rename, head was None) are full
|
|
431
|
+
// reads; read 3 hits the cache primed by read 2 and reads after position "1".
|
|
432
|
+
expect((r3, mock.readAfters.contents, mock.getEvents()->Array.length))->toEqual((
|
|
433
|
+
[Ok("ref-3")],
|
|
434
|
+
[None, None, Some("1")],
|
|
435
|
+
3,
|
|
436
|
+
))
|
|
437
|
+
},
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
testPromise(
|
|
441
|
+
"a different entity does not hit another entity's cache (full read)",
|
|
442
|
+
async () => {
|
|
443
|
+
let run = (reference, command) =>
|
|
444
|
+
TestHandler.handleCommands(
|
|
445
|
+
testDcbEventLog,
|
|
446
|
+
Stream.fromIterable([makeTopicItem(reference, command)]),
|
|
447
|
+
)->Effect.runPromise
|
|
448
|
+
|
|
449
|
+
let _ = await run(
|
|
450
|
+
"ref-1",
|
|
451
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "A"}),
|
|
452
|
+
)
|
|
453
|
+
let _ = await run(
|
|
454
|
+
"ref-2",
|
|
455
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-2", name: "B"}),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
// item-2's query is a distinct cache key → cold full read (after=None).
|
|
459
|
+
expect(mock.readAfters.contents)->toEqual([None, None])
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
testPromise(
|
|
464
|
+
"conflict re-seeds the cache so the retry reads the delta",
|
|
465
|
+
async () => {
|
|
466
|
+
let run = (reference, command) =>
|
|
467
|
+
TestHandler.handleCommands(
|
|
468
|
+
testDcbEventLog,
|
|
469
|
+
Stream.fromIterable([makeTopicItem(reference, command)]),
|
|
470
|
+
)->Effect.runPromise
|
|
471
|
+
|
|
472
|
+
let _ = await run(
|
|
473
|
+
"ref-1",
|
|
474
|
+
DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "A"}),
|
|
475
|
+
)
|
|
476
|
+
// Force one append failure: the retry must re-seed from the just-read
|
|
477
|
+
// snapshot and read only the delta after position "1".
|
|
478
|
+
mock.failNextAppends := 1
|
|
479
|
+
let r2 = await run(
|
|
480
|
+
"ref-2",
|
|
481
|
+
DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "B"}),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
expect((r2, mock.readAfters.contents, mock.getEvents()->Array.length))->toEqual((
|
|
485
|
+
[Ok("ref-2")],
|
|
486
|
+
[None, None, Some("1")],
|
|
487
|
+
2,
|
|
488
|
+
))
|
|
489
|
+
},
|
|
490
|
+
)
|
|
362
491
|
})
|
|
363
492
|
})
|