@reventlessdev/reventless-core 3.0.0-alpha.260 → 3.0.0-alpha.262
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 +24 -0
- package/package.json +10 -10
- 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/adapter/Secrets/Secrets_Node_Backend.res +60 -0
- package/src/adapter/Secrets/Secrets_Node_Backend.res.mjs +69 -0
- 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 +38 -51
- 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 +28 -23
- package/src/components/Api/SuryToJsonSchema.res +9 -10
- 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/Secrets_Node_BackendTest.res +141 -0
- package/tests/Secrets_Node_BackendTest.res.mjs +152 -0
- 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 +22 -14
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +4 -3
- 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 +1315 -1014
- 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 +1529 -1045
- 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 +4 -4
- 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
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
open JestGlobals
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
module AggSpec = {
|
|
8
7
|
module Id = Reventless.Id.StringPure
|
|
9
8
|
let name = "ConflictTestAggregate"
|
|
10
9
|
|
|
11
10
|
@schema
|
|
12
|
-
type command =
|
|
11
|
+
type command = Create({name: string})
|
|
13
12
|
|
|
14
13
|
@schema
|
|
15
|
-
type event =
|
|
14
|
+
type event = Created({name: string})
|
|
16
15
|
|
|
17
16
|
@schema
|
|
18
|
-
type error =
|
|
17
|
+
type error = AlreadyExists
|
|
19
18
|
|
|
20
19
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
21
20
|
}
|
|
@@ -37,14 +36,21 @@ module TestBehavior = {
|
|
|
37
36
|
| Created({name}) => {name: name}
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
let decide = (_state: state, command: AggSpec.command): result<
|
|
39
|
+
let decide = (_state: state, command: AggSpec.command): result<
|
|
40
|
+
array<AggSpec.event>,
|
|
41
|
+
AggSpec.error,
|
|
42
|
+
> =>
|
|
41
43
|
switch command {
|
|
42
44
|
| Create({name}) => Ok([AggSpec.Created({name: name})])
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
type mockEL = {
|
|
47
|
-
appendFn: (
|
|
49
|
+
appendFn: (
|
|
50
|
+
int,
|
|
51
|
+
string,
|
|
52
|
+
array<Message.event'<string, AggSpec.event>>,
|
|
53
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
48
54
|
replayFn: string => promise<array<AggSpec.event>>,
|
|
49
55
|
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
50
56
|
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
@@ -67,7 +73,7 @@ let makeMockEL = (): mockEL => {
|
|
|
67
73
|
Error(EventLog.Conflict)
|
|
68
74
|
} else {
|
|
69
75
|
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
70
|
-
Ok(
|
|
76
|
+
Ok()
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -117,10 +123,18 @@ module TestOps = {
|
|
|
117
123
|
type event = AggSpec.event
|
|
118
124
|
}
|
|
119
125
|
type operations = {
|
|
120
|
-
append: (
|
|
126
|
+
append: (
|
|
127
|
+
int,
|
|
128
|
+
string,
|
|
129
|
+
array<Message.event'<string, AggSpec.event>>,
|
|
130
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
121
131
|
replay: string => promise<array<AggSpec.event>>,
|
|
122
132
|
replayStream: (string, ~fromSeq: int=?) => Stream.t<AggSpec.event, string, unit>,
|
|
123
|
-
appendStream: (
|
|
133
|
+
appendStream: (
|
|
134
|
+
int,
|
|
135
|
+
string,
|
|
136
|
+
Stream.t<AggSpec.event, string, unit>,
|
|
137
|
+
) => Effect.t<unit, string, unit>,
|
|
124
138
|
latestSnapshot: string => promise<result<option<EventLog.snapshot>, string>>,
|
|
125
139
|
writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
|
|
126
140
|
}
|
|
@@ -169,7 +183,9 @@ describe("Aggregate_Callback — conflict retry:", () => {
|
|
|
169
183
|
mock.failNextAppendsWithConflict := 1
|
|
170
184
|
let results = await Stream.fromIterable([
|
|
171
185
|
makeTopicItem("ref-1", AggSpec.Create({name: "Widget"})),
|
|
172
|
-
])
|
|
186
|
+
])
|
|
187
|
+
->TestHandler.handleCommands
|
|
188
|
+
->Effect.runPromise
|
|
173
189
|
expect(results)->toEqual([Ok("ref-1")])
|
|
174
190
|
// 1 initial append (conflict) + 1 retry append (success) = 2
|
|
175
191
|
expect(mock.appendCallCount.contents)->toBe(2)
|
|
@@ -177,9 +193,9 @@ describe("Aggregate_Callback — conflict retry:", () => {
|
|
|
177
193
|
|
|
178
194
|
testPromise("conflict triggers re-replay", async () => {
|
|
179
195
|
mock.failNextAppendsWithConflict := 1
|
|
180
|
-
let _ = await Stream.fromIterable([
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
let _ = await Stream.fromIterable([makeTopicItem("ref-1", AggSpec.Create({name: "Widget"}))])
|
|
197
|
+
->TestHandler.handleCommands
|
|
198
|
+
->Effect.runPromise
|
|
183
199
|
// 1 initial replay + 1 retry replay = 2
|
|
184
200
|
expect(mock.replayCallCount.contents)->toBe(2)
|
|
185
201
|
})
|
|
@@ -188,7 +204,9 @@ describe("Aggregate_Callback — conflict retry:", () => {
|
|
|
188
204
|
mock.failNextAppendsWithConflict := 2
|
|
189
205
|
let results = await Stream.fromIterable([
|
|
190
206
|
makeTopicItem("ref-1", AggSpec.Create({name: "Widget"})),
|
|
191
|
-
])
|
|
207
|
+
])
|
|
208
|
+
->TestHandler.handleCommands
|
|
209
|
+
->Effect.runPromise
|
|
192
210
|
expect(results)->toEqual([Ok("ref-1")])
|
|
193
211
|
expect(mock.appendCallCount.contents)->toBe(3)
|
|
194
212
|
})
|
|
@@ -198,23 +216,27 @@ describe("Aggregate_Callback — conflict retry:", () => {
|
|
|
198
216
|
mock.failNextAppendsWithConflict := 4
|
|
199
217
|
let results = await Stream.fromIterable([
|
|
200
218
|
makeTopicItem("ref-1", AggSpec.Create({name: "Widget"})),
|
|
201
|
-
])
|
|
219
|
+
])
|
|
220
|
+
->TestHandler.handleCommands
|
|
221
|
+
->Effect.runPromise
|
|
202
222
|
expect(results)->toEqual([Error("concurrent modification (3 retries exhausted): conflict")])
|
|
203
223
|
expect(mock.appendCallCount.contents)->toBe(4)
|
|
204
224
|
})
|
|
205
225
|
|
|
206
226
|
testPromise("successful append after conflict stores event", async () => {
|
|
207
227
|
mock.failNextAppendsWithConflict := 1
|
|
208
|
-
let _ = await Stream.fromIterable([
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
let _ = await Stream.fromIterable([makeTopicItem("ref-1", AggSpec.Create({name: "Widget"}))])
|
|
229
|
+
->TestHandler.handleCommands
|
|
230
|
+
->Effect.runPromise
|
|
211
231
|
expect(mock.getAll()->Array.length)->toBe(1)
|
|
212
232
|
})
|
|
213
233
|
|
|
214
234
|
testPromise("no conflict does not retry", async () => {
|
|
215
235
|
let results = await Stream.fromIterable([
|
|
216
236
|
makeTopicItem("ref-1", AggSpec.Create({name: "Widget"})),
|
|
217
|
-
])
|
|
237
|
+
])
|
|
238
|
+
->TestHandler.handleCommands
|
|
239
|
+
->Effect.runPromise
|
|
218
240
|
expect(results)->toEqual([Ok("ref-1")])
|
|
219
241
|
expect(mock.appendCallCount.contents)->toBe(1)
|
|
220
242
|
expect(mock.replayCallCount.contents)->toBe(1)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
// ─────────────────────────────────────────────────────────────
|
|
3
2
|
// Aggregate spec
|
|
4
3
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -49,7 +48,10 @@ module TestBehavior = {
|
|
|
49
48
|
| AggSpec.Renamed({newName}) => Created({name: newName})
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
let decide = (state: state, command: AggSpec.command): result<
|
|
51
|
+
let decide = (state: state, command: AggSpec.command): result<
|
|
52
|
+
array<AggSpec.event>,
|
|
53
|
+
AggSpec.error,
|
|
54
|
+
> =>
|
|
53
55
|
switch (state, command) {
|
|
54
56
|
| (NotCreated, Create({name})) => Ok([AggSpec.Created({name: name})])
|
|
55
57
|
| (NotCreated, Rename(_)) => Error(NotFound)
|
|
@@ -63,7 +65,11 @@ module TestBehavior = {
|
|
|
63
65
|
// ─────────────────────────────────────────────────────────────
|
|
64
66
|
|
|
65
67
|
type mockEL = {
|
|
66
|
-
appendFn: (
|
|
68
|
+
appendFn: (
|
|
69
|
+
int,
|
|
70
|
+
string,
|
|
71
|
+
array<Message.event'<string, AggSpec.event>>,
|
|
72
|
+
) => promise<result<unit, EventLog.appendError>>,
|
|
67
73
|
replayFn: string => promise<array<AggSpec.event>>,
|
|
68
74
|
replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
|
|
69
75
|
getAll: unit => array<Message.event'<string, AggSpec.event>>,
|
|
@@ -81,7 +87,7 @@ let makeMockEL = (): mockEL => {
|
|
|
81
87
|
Error(EventLog.StorageFailure("append failed"))
|
|
82
88
|
} else {
|
|
83
89
|
storedRef := storedRef.contents->Array.concat(newEvents)
|
|
84
|
-
Ok(
|
|
90
|
+
Ok()
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
let replayFn = async id =>
|
|
@@ -135,7 +141,11 @@ module TestOps = {
|
|
|
135
141
|
) => promise<result<unit, EventLog.appendError>>,
|
|
136
142
|
replay: string => promise<array<AggSpec.event>>,
|
|
137
143
|
replayStream: (string, ~fromSeq: int=?) => Stream.t<AggSpec.event, string, unit>,
|
|
138
|
-
appendStream: (
|
|
144
|
+
appendStream: (
|
|
145
|
+
int,
|
|
146
|
+
string,
|
|
147
|
+
Stream.t<AggSpec.event, string, unit>,
|
|
148
|
+
) => Effect.t<unit, string, unit>,
|
|
139
149
|
latestSnapshot: string => promise<result<option<EventLog.snapshot>, string>>,
|
|
140
150
|
writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
|
|
141
151
|
}
|
|
@@ -34,109 +34,126 @@ let uninstallChannels = () => {
|
|
|
34
34
|
|
|
35
35
|
describe("Aggregate decide-rejection propagation:", () => {
|
|
36
36
|
describe("single rejected command", () => {
|
|
37
|
-
testPromise(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
testPromise(
|
|
38
|
+
"Create on existing aggregate reports Rejected with errorCode",
|
|
39
|
+
async () => {
|
|
40
|
+
// Seed: aggregate already created → second Create rejects with AlreadyExists.
|
|
41
|
+
let _ = await Stream.fromIterable([
|
|
41
42
|
makeTopicItem("ref-seed", AggSpec.Create({name: "Widget"})),
|
|
42
|
-
])
|
|
43
|
+
])
|
|
44
|
+
->TestHandler.handleCommands
|
|
45
|
+
->Effect.runPromise
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await Stream.fromIterable([
|
|
47
|
+
installChannels()
|
|
48
|
+
let results = await Stream.fromIterable([
|
|
47
49
|
makeTopicItem("ref-1", AggSpec.Create({name: "Widget"})),
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
+
])
|
|
51
|
+
->TestHandler.handleCommands
|
|
52
|
+
->Effect.runPromise
|
|
53
|
+
uninstallChannels()
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
// Producer sees Rejected on the channel; SQS sees Ok (delete — don't redeliver).
|
|
56
|
+
expect(results)->toEqual([Ok("ref-1")])
|
|
57
|
+
expect(capturedRejections.contents->Array.length)->toBe(1)
|
|
58
|
+
let (ref, info) = capturedRejections.contents->Array.getUnsafe(0)
|
|
59
|
+
expect(ref)->toEqual("ref-1")
|
|
60
|
+
expect(info.errorCode)->toEqual("AlreadyExists")
|
|
61
|
+
// No event was appended.
|
|
62
|
+
expect(mock.getAll()->Array.length)->toBe(1)
|
|
63
|
+
},
|
|
64
|
+
)
|
|
60
65
|
})
|
|
61
66
|
|
|
62
67
|
describe("mixed batch", () => {
|
|
63
|
-
testPromise(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
testPromise(
|
|
69
|
+
"rejected command does not cancel surviving commands",
|
|
70
|
+
async () => {
|
|
71
|
+
// Seed.
|
|
72
|
+
let _ = await Stream.fromIterable([
|
|
67
73
|
makeTopicItem(~aggId="agg-1", "ref-seed", AggSpec.Create({name: "Widget"})),
|
|
68
|
-
])
|
|
74
|
+
])
|
|
75
|
+
->TestHandler.handleCommands
|
|
76
|
+
->Effect.runPromise
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await Stream.fromIterable([
|
|
78
|
+
installChannels()
|
|
79
|
+
// Batch on the same aggregate: Rename (ok) → Create (rejects) → Rename (ok)
|
|
80
|
+
let results = await Stream.fromIterable([
|
|
74
81
|
makeTopicItem(~aggId="agg-1", "ref-1", AggSpec.Rename({newName: "First"})),
|
|
75
82
|
makeTopicItem(~aggId="agg-1", "ref-2", AggSpec.Create({name: "Boom"})),
|
|
76
83
|
makeTopicItem(~aggId="agg-1", "ref-3", AggSpec.Rename({newName: "Last"})),
|
|
77
|
-
])
|
|
78
|
-
|
|
84
|
+
])
|
|
85
|
+
->TestHandler.handleCommands
|
|
86
|
+
->Effect.runPromise
|
|
87
|
+
uninstallChannels()
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
// All three references map to Ok (rejected ones still Ok — SQS deletes).
|
|
90
|
+
expect(results->Array.length)->toBe(3)
|
|
91
|
+
results->Array.forEach(
|
|
92
|
+
r =>
|
|
93
|
+
switch r {
|
|
94
|
+
| Ok(_) => ()
|
|
95
|
+
| Error(_) => expect("expected all Ok")->toEqual("got Error")
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
// Two Renamed events appended (the rejected Create produced none).
|
|
99
|
+
// Seed = 1, plus 2 Renamed = 3 stored.
|
|
100
|
+
expect(mock.getAll()->Array.length)->toBe(3)
|
|
101
|
+
// ref-2 reported as Rejected with AlreadyExists.
|
|
102
|
+
let rejected = capturedRejections.contents
|
|
103
|
+
expect(rejected->Array.length)->toBe(1)
|
|
104
|
+
let (rRef, rInfo) = rejected->Array.getUnsafe(0)
|
|
105
|
+
expect(rRef)->toEqual("ref-2")
|
|
106
|
+
expect(rInfo.errorCode)->toEqual("AlreadyExists")
|
|
107
|
+
// ref-1 and ref-3 reported as Accepted (eventCount > 0 against the appended batch).
|
|
108
|
+
let accepted = capturedAccepts.contents
|
|
109
|
+
expect(accepted->Array.length)->toBe(2)
|
|
110
|
+
},
|
|
111
|
+
)
|
|
101
112
|
})
|
|
102
113
|
|
|
103
114
|
describe("all-reject batch", () => {
|
|
104
|
-
testPromise(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
testPromise(
|
|
116
|
+
"3 rejecting commands → no append, all reported Rejected, no Accepted",
|
|
117
|
+
async () => {
|
|
118
|
+
// No seed → empty history. Rename on a NotCreated aggregate rejects with NotFound.
|
|
119
|
+
installChannels()
|
|
120
|
+
let results = await Stream.fromIterable([
|
|
109
121
|
makeTopicItem(~aggId="agg-x", "ref-1", AggSpec.Rename({newName: "A"})),
|
|
110
122
|
makeTopicItem(~aggId="agg-x", "ref-2", AggSpec.Rename({newName: "B"})),
|
|
111
123
|
makeTopicItem(~aggId="agg-x", "ref-3", AggSpec.Rename({newName: "C"})),
|
|
112
|
-
])
|
|
113
|
-
|
|
124
|
+
])
|
|
125
|
+
->TestHandler.handleCommands
|
|
126
|
+
->Effect.runPromise
|
|
127
|
+
uninstallChannels()
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
expect(results)->toEqual([Ok("ref-1"), Ok("ref-2"), Ok("ref-3")])
|
|
130
|
+
// No events appended.
|
|
131
|
+
expect(mock.getAll()->Array.length)->toBe(0)
|
|
132
|
+
// All three reported as Rejected.
|
|
133
|
+
expect(capturedRejections.contents->Array.length)->toBe(3)
|
|
134
|
+
capturedRejections.contents->Array.forEach(
|
|
135
|
+
((_ref, info)) => expect(info.errorCode)->toEqual("NotFound"),
|
|
136
|
+
)
|
|
137
|
+
// No Accepted callbacks fired (when nothing is appended and nothing is OK).
|
|
138
|
+
expect(capturedAccepts.contents->Array.length)->toBe(0)
|
|
139
|
+
},
|
|
140
|
+
)
|
|
126
141
|
})
|
|
127
142
|
|
|
128
143
|
describe("payload-less variant errors", () => {
|
|
129
|
-
testPromise(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
testPromise(
|
|
145
|
+
"errorDetail is empty string for unit-payload variants",
|
|
146
|
+
async () => {
|
|
147
|
+
// AggSpec.error variants are all payload-less (NotFound | AlreadyExists).
|
|
148
|
+
installChannels()
|
|
149
|
+
let _ = await Stream.fromIterable([makeTopicItem("ref-1", AggSpec.Rename({newName: "X"}))])
|
|
150
|
+
->TestHandler.handleCommands
|
|
151
|
+
->Effect.runPromise
|
|
152
|
+
uninstallChannels()
|
|
137
153
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
154
|
+
let (_ref, info) = capturedRejections.contents->Array.getUnsafe(0)
|
|
155
|
+
expect(info.errorDetail)->toEqual("")
|
|
156
|
+
},
|
|
157
|
+
)
|
|
141
158
|
})
|
|
142
159
|
})
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
open JestGlobals
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
module AggSpec = {
|
|
17
16
|
module Id = Reventless.Id.StringPure
|
|
18
17
|
let name = "SnapshotTestAggregate"
|
|
@@ -24,7 +23,7 @@ module AggSpec = {
|
|
|
24
23
|
type event = Added({item: string})
|
|
25
24
|
|
|
26
25
|
@schema
|
|
27
|
-
type error =
|
|
26
|
+
type error = Never
|
|
28
27
|
|
|
29
28
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
30
29
|
}
|
|
@@ -161,7 +160,11 @@ let mock = makeMockEL()
|
|
|
161
160
|
|
|
162
161
|
module OuterEventLog = EventLog
|
|
163
162
|
|
|
164
|
-
module MakeOps = (
|
|
163
|
+
module MakeOps = (
|
|
164
|
+
B: {
|
|
165
|
+
let name: string
|
|
166
|
+
},
|
|
167
|
+
) => {
|
|
165
168
|
module Spec = AggSpec
|
|
166
169
|
module EventLog = {
|
|
167
170
|
module Spec = {
|
|
@@ -178,11 +181,11 @@ module MakeOps = (B: {let name: string}) => {
|
|
|
178
181
|
) => promise<result<unit, EventLog.appendError>>,
|
|
179
182
|
replay: string => promise<array<AggSpec.event>>,
|
|
180
183
|
replayStream: (string, ~fromSeq: int=?) => Stream.t<AggSpec.event, string, unit>,
|
|
181
|
-
appendStream: (
|
|
182
|
-
|
|
184
|
+
appendStream: (
|
|
185
|
+
int,
|
|
183
186
|
string,
|
|
184
|
-
unit
|
|
185
|
-
>,
|
|
187
|
+
Stream.t<AggSpec.event, string, unit>,
|
|
188
|
+
) => Effect.t<unit, string, unit>,
|
|
186
189
|
latestSnapshot: string => promise<result<option<EventLog.snapshot>, string>>,
|
|
187
190
|
writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
|
|
188
191
|
}
|
|
@@ -199,7 +202,9 @@ module MakeOps = (B: {let name: string}) => {
|
|
|
199
202
|
}
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
module TestOps = MakeOps({
|
|
205
|
+
module TestOps = MakeOps({
|
|
206
|
+
let name = "SnapshotMockEventLog"
|
|
207
|
+
})
|
|
203
208
|
module TestHandler = Aggregate_Callback.Make(AggSpec, TestBehavior, TestOps)
|
|
204
209
|
|
|
205
210
|
// A second behavior with snapshots DISABLED, sharing the same mock, to prove
|
|
@@ -225,7 +230,9 @@ module PlainBehavior = {
|
|
|
225
230
|
}
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
module PlainOps = MakeOps({
|
|
233
|
+
module PlainOps = MakeOps({
|
|
234
|
+
let name = "PlainMockEventLog"
|
|
235
|
+
})
|
|
229
236
|
module PlainHandler = Aggregate_Callback.Make(AggSpec, PlainBehavior, PlainOps)
|
|
230
237
|
|
|
231
238
|
let makeItem = (~id="agg-1", reference, command): CommandTopic.topicItem<
|
|
@@ -253,7 +260,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
253
260
|
testPromise("writes a snapshot each time an interval boundary is crossed", async () => {
|
|
254
261
|
// 25 single-event commands → boundaries at 10 and 20 (not 25).
|
|
255
262
|
for i in 1 to 25 {
|
|
256
|
-
let _ = await run(
|
|
263
|
+
let _ = await run(
|
|
264
|
+
~handler=TestHandler.handleCommands,
|
|
265
|
+
AggSpec.Add({item: `i${i->Int.toString}`}),
|
|
266
|
+
)
|
|
257
267
|
}
|
|
258
268
|
let _ = await tick()
|
|
259
269
|
expect(mock.snapshotWriteCount.contents)->toBe(2)
|
|
@@ -266,7 +276,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
266
276
|
|
|
267
277
|
testPromise("cold replay seeds from the snapshot and reads only the delta", async () => {
|
|
268
278
|
for i in 1 to 12 {
|
|
269
|
-
let _ = await run(
|
|
279
|
+
let _ = await run(
|
|
280
|
+
~handler=TestHandler.handleCommands,
|
|
281
|
+
AggSpec.Add({item: `i${i->Int.toString}`}),
|
|
282
|
+
)
|
|
270
283
|
}
|
|
271
284
|
let _ = await tick()
|
|
272
285
|
// A snapshot exists at seq 10. Evict the in-process cache to force a cold read.
|
|
@@ -282,7 +295,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
282
295
|
|
|
283
296
|
testPromise("state seeded from snapshot equals a full-replay fold", async () => {
|
|
284
297
|
for i in 1 to 15 {
|
|
285
|
-
let _ = await run(
|
|
298
|
+
let _ = await run(
|
|
299
|
+
~handler=TestHandler.handleCommands,
|
|
300
|
+
AggSpec.Add({item: `x${i->Int.toString}`}),
|
|
301
|
+
)
|
|
286
302
|
}
|
|
287
303
|
let _ = await tick()
|
|
288
304
|
// Snapshot-seeded cold read (snapshot at seq 10, delta 10..15).
|
|
@@ -292,7 +308,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
292
308
|
// Force a boundary to capture the seeded state as a fresh snapshot: add up to
|
|
293
309
|
// seq 20 and compare its item count to the true event count.
|
|
294
310
|
for i in 16 to 20 {
|
|
295
|
-
let _ = await run(
|
|
311
|
+
let _ = await run(
|
|
312
|
+
~handler=TestHandler.handleCommands,
|
|
313
|
+
AggSpec.Add({item: `x${i->Int.toString}`}),
|
|
314
|
+
)
|
|
296
315
|
}
|
|
297
316
|
let _ = await tick()
|
|
298
317
|
switch mock.getSnapshot("agg-1") {
|
|
@@ -312,7 +331,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
312
331
|
|
|
313
332
|
testPromise("schema-drift snapshot is ignored → full replay from 0", async () => {
|
|
314
333
|
for i in 1 to 12 {
|
|
315
|
-
let _ = await run(
|
|
334
|
+
let _ = await run(
|
|
335
|
+
~handler=TestHandler.handleCommands,
|
|
336
|
+
AggSpec.Add({item: `i${i->Int.toString}`}),
|
|
337
|
+
)
|
|
316
338
|
}
|
|
317
339
|
let _ = await tick()
|
|
318
340
|
// Corrupt the stored snapshot's hash to simulate a state-shape change.
|
|
@@ -329,7 +351,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
329
351
|
|
|
330
352
|
testPromise("snapshot write failure does not fail the command", async () => {
|
|
331
353
|
for i in 1 to 9 {
|
|
332
|
-
let _ = await run(
|
|
354
|
+
let _ = await run(
|
|
355
|
+
~handler=TestHandler.handleCommands,
|
|
356
|
+
AggSpec.Add({item: `i${i->Int.toString}`}),
|
|
357
|
+
)
|
|
333
358
|
}
|
|
334
359
|
// The 10th append crosses the boundary; make its snapshot write fail.
|
|
335
360
|
mock.failNextWrite := true
|
|
@@ -344,7 +369,10 @@ describe("Aggregate_Callback — snapshots:", () => {
|
|
|
344
369
|
|
|
345
370
|
testPromise("disabled aggregate does zero snapshot reads and writes", async () => {
|
|
346
371
|
for i in 1 to 12 {
|
|
347
|
-
let _ = await run(
|
|
372
|
+
let _ = await run(
|
|
373
|
+
~handler=PlainHandler.handleCommands,
|
|
374
|
+
AggSpec.Add({item: `i${i->Int.toString}`}),
|
|
375
|
+
)
|
|
348
376
|
}
|
|
349
377
|
let _ = await tick()
|
|
350
378
|
PlainHandler.resetCache()
|
|
@@ -40,8 +40,9 @@ describe("Api_Ids.encode / decode", () => {
|
|
|
40
40
|
|
|
41
41
|
describe("Api_Ids.alternateKey — the fallback a door tries on a miss", () => {
|
|
42
42
|
testSync("a global id offers the storage key inside it", () =>
|
|
43
|
-
expect(
|
|
44
|
-
|
|
43
|
+
expect(
|
|
44
|
+
Api_Ids.alternateKey(Api_Ids.encode(~typeName="Catalog_Product", ~localId="p-1")),
|
|
45
|
+
)->toEqual(Some("p-1"))
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
testSync("a plain key offers nothing, so the lookup is never retried", () =>
|
|
@@ -83,8 +84,13 @@ describe("stamping an id onto a row leaves the stored row alone", () => {
|
|
|
83
84
|
JSON.Encode.object(obj)
|
|
84
85
|
}
|
|
85
86
|
let idOf = j =>
|
|
86
|
-
j
|
|
87
|
-
|
|
87
|
+
j
|
|
88
|
+
->JSON.Decode.object
|
|
89
|
+
->Option.flatMap(d => d->Dict.get("id"))
|
|
90
|
+
->Option.flatMap(JSON.Decode.string)
|
|
91
|
+
expect(idOf(stamped))->toEqual(
|
|
92
|
+
Some(Api_Ids.encode(~typeName="Ordering_Customer", ~localId="cust-42")),
|
|
93
|
+
)
|
|
88
94
|
// The stored row still carries its storage key.
|
|
89
95
|
expect(idOf(row))->toEqual(Some("cust-42"))
|
|
90
96
|
})
|