@reventlessdev/reventless-core 3.0.0-alpha.260 → 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.
Files changed (203) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +9 -9
  3. package/src/Behavior.res +1 -4
  4. package/src/Message.res +17 -14
  5. package/src/Projection.res +51 -25
  6. package/src/RequestContext.res +8 -9
  7. package/src/ResourceAttribution.res +28 -14
  8. package/src/adapter/Adapter.res +48 -4
  9. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
  10. package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
  11. package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
  12. package/src/adapter/Monitoring/Monitoring.res +16 -17
  13. package/src/adapter/QueryInterception/QueryInterception.res +0 -1
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
  15. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
  16. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
  17. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
  18. package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
  19. package/src/admin/Platform_Admin.res +17 -6
  20. package/src/admin/Platform_AdminApi.res +14 -9
  21. package/src/admin/Platform_Admin_Callback.res +1 -1
  22. package/src/admin/Platform_BakedManifest.res +34 -34
  23. package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
  24. package/src/admin/Platform_PluginStructuresApi.res +15 -12
  25. package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
  26. package/src/components/Aggregate/Aggregate.res +13 -12
  27. package/src/components/Aggregate/Aggregate_Builder.res +22 -21
  28. package/src/components/Aggregate/Aggregate_Callback.res +91 -86
  29. package/src/components/Api/ApiNoApiHelpers.res +1 -3
  30. package/src/components/Api/Api_Ids.res +0 -1
  31. package/src/components/Api/Api_Naming.res +41 -18
  32. package/src/components/Api/GraphQL_FragmentGenerator.res +38 -51
  33. package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
  34. package/src/components/Api/GraphQL_Stitcher.res +9 -10
  35. package/src/components/Api/MCP_SchemaGenerator.res +8 -12
  36. package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
  37. package/src/components/Api/QueryDbListQuery.res +13 -6
  38. package/src/components/Api/SchemaType.res +28 -23
  39. package/src/components/Api/SuryToJsonSchema.res +9 -10
  40. package/src/components/AutomationSlice/AutomationSlice.res +4 -4
  41. package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
  42. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
  43. package/src/components/Builder_Helpers.res +12 -23
  44. package/src/components/CommandGenerator/CommandGenerator.res +6 -1
  45. package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
  46. package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
  47. package/src/components/CommandTopic/CommandTopic.res +3 -3
  48. package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
  49. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
  50. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
  51. package/src/components/Counter/Counter_Builder.res +1 -6
  52. package/src/components/Counter/Counter_Callback.res +3 -3
  53. package/src/components/Dcb/Dcb_Builder.res +970 -956
  54. package/src/components/DcbEventLog/DcbEventLog.res +4 -4
  55. package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
  56. package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
  57. package/src/components/EventCollector/EventCollector_Builder.res +2 -1
  58. package/src/components/EventLog/EventLog.res +5 -1
  59. package/src/components/EventLog/EventLog_Operations.res +12 -8
  60. package/src/components/EventMapper/EventMapper.res +8 -10
  61. package/src/components/EventMapper/EventMapper_Builder.res +4 -1
  62. package/src/components/EventMapper/EventMapper_Callback.res +46 -31
  63. package/src/components/EventTopic/EventTopic.res +3 -3
  64. package/src/components/EventTopic/EventTopic_Builder.res +1 -1
  65. package/src/components/Extension/Extension.res +4 -1
  66. package/src/components/Extension/Extension_Operations.res +11 -3
  67. package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
  68. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
  69. package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
  70. package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
  71. package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
  72. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
  73. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
  74. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
  75. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
  76. package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
  77. package/src/components/QueryDb/QueryDb_Operations.res +10 -10
  78. package/src/components/ReadModel/ReadModel.res +3 -3
  79. package/src/components/ReadModel/ReadModel_Builder.res +1 -2
  80. package/src/components/ReadModel/ReadModel_Callback.res +2 -1
  81. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
  82. package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
  83. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
  84. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
  85. package/src/components/StateTopic.res +1 -3
  86. package/src/components/StateViewSlice/StateViewSlice.res +4 -4
  87. package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
  88. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  89. package/src/components/Task/Task.res +3 -3
  90. package/src/components/Task/Task_Adapter.res +4 -1
  91. package/src/components/Task/Task_Builder.res +24 -21
  92. package/src/plugin/api/PluginBaseFragment.res +9 -3
  93. package/src/plugin/component/Capability_Inference.res +8 -10
  94. package/src/plugin/component/Plugin.res +6 -6
  95. package/src/plugin/component/Plugin_Builder.res +54 -50
  96. package/src/plugin/component/Plugin_Callback.res +10 -6
  97. package/src/plugin/component/Plugin_Helpers.res +264 -230
  98. package/src/plugin/component/Plugin_Structure.res +370 -324
  99. package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
  100. package/src/plugin/component/SchemaWalker.res +11 -12
  101. package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
  102. package/src/plugin/lifecycle/PluginBehavior.res +3 -4
  103. package/src/plugin/lifecycle/PluginSpec.res +1 -2
  104. package/src/plugin/lifecycle/PluginsProjection.res +16 -5
  105. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  106. package/src/util/CommandPublisher.res +9 -2
  107. package/src/util/CsvStream.res +3 -3
  108. package/src/util/Interstack.res +7 -15
  109. package/src/util/Interstack.resi +3 -3
  110. package/src/util/LogFormat.res +5 -9
  111. package/src/util/Logger.res +1 -2
  112. package/src/util/Lru.res +2 -1
  113. package/src/util/Lru.resi +2 -3
  114. package/src/util/ScheduleOps.res +24 -27
  115. package/src/util/StorePrefixCollision.res +6 -9
  116. package/src/util/Util_Promise.res +4 -1
  117. package/src/util/Util_Promise.res.mjs +1 -1
  118. package/tests/IdentityTest.res +166 -113
  119. package/tests/ResourceAttributionTest.res +28 -25
  120. package/tests/StorePrefixCollisionTest.res +2 -1
  121. package/tests/TaskBucketNamingTest.res +3 -2
  122. package/tests/adapter/EventLogProvisioningTest.res +51 -48
  123. package/tests/adapter/MonitoringTest.res +81 -38
  124. package/tests/adapter/QueryInterceptionTest.res +2 -2
  125. package/tests/adapter/RefusalVocabularyTest.res +31 -23
  126. package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
  127. package/tests/adapter/RuntimeExtensionTest.res +5 -3
  128. package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
  129. package/tests/admin/Platform_Admin_StructureTest.res +10 -11
  130. package/tests/admin/Platform_BakedManifestTest.res +31 -35
  131. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
  132. package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
  133. package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
  134. package/tests/aggregate/AggregateCacheTest.res +10 -12
  135. package/tests/aggregate/AggregateCallbackTest.res +75 -46
  136. package/tests/aggregate/AggregateCausationTest.res +19 -20
  137. package/tests/aggregate/AggregateConflictTest.res +41 -19
  138. package/tests/aggregate/AggregateFixtures.res +15 -5
  139. package/tests/aggregate/AggregateRejectionTest.res +97 -80
  140. package/tests/aggregate/AggregateSnapshotTest.res +44 -16
  141. package/tests/api/Api_IdsTest.res +10 -4
  142. package/tests/api/BrandedMarkerCompositionTest.res +48 -36
  143. package/tests/api/GeolocationSemanticTest.res +3 -3
  144. package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
  145. package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
  146. package/tests/api/StorageRefFieldsTest.res +30 -18
  147. package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
  148. package/tests/api/TaggedUnionSchemaTest.res +17 -24
  149. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
  150. package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
  151. package/tests/commandgenerator/OwnerStampingTest.res +91 -76
  152. package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
  153. package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
  154. package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
  155. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
  156. package/tests/counter/CounterCallbackTest.res +40 -34
  157. package/tests/counter/CounterFixtures.res +4 -4
  158. package/tests/dcb/DcbDecodeTest.res +115 -84
  159. package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
  160. package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
  161. package/tests/dcb/DcbFixtures.res +4 -5
  162. package/tests/dcb/DcbScopeInferenceTest.res +173 -114
  163. package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
  164. package/tests/dcb/DcbTagTest.res +22 -11
  165. package/tests/dcb/DcbValidationTest.res +288 -218
  166. package/tests/eventlog/EventLogFixtures.res +4 -4
  167. package/tests/eventlog/EventLogOperationsTest.res +102 -67
  168. package/tests/eventlog/EventLogRetryTest.res +104 -65
  169. package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
  170. package/tests/eventmapper/EventMapperFixtures.res +15 -23
  171. package/tests/eventmapper/EventMapperTest.res +54 -38
  172. package/tests/eventtopic/EventTopicFixtures.res +3 -4
  173. package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
  174. package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
  175. package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
  176. package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
  177. package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
  178. package/tests/logger/LogFormatTest.res +104 -82
  179. package/tests/logger/LoggerLazyTest.res +21 -12
  180. package/tests/message/MessageTest.res +96 -93
  181. package/tests/message/MetaEnvelopeTest.res +26 -30
  182. package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
  183. package/tests/plugin/ManifestVisibilityTest.res +20 -12
  184. package/tests/plugin/PluginDefinitionScalars.res +1 -2
  185. package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
  186. package/tests/plugin/PluginSpecExperiment.res +0 -1
  187. package/tests/plugin/PluginStructureTest.res +1529 -1045
  188. package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
  189. package/tests/plugin/SchemaWalkerTest.res +116 -70
  190. package/tests/plugin/StateChange/PsReserveStock.res +4 -4
  191. package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
  192. package/tests/plugin/StateView/PsCustomersView.res +4 -4
  193. package/tests/plugin/StateView/PsShipmentsView.res +3 -2
  194. package/tests/projection/LifecycleTrailTest.res +1 -4
  195. package/tests/projection/ProjectionOptimizeTest.res +13 -9
  196. package/tests/querydb/QueryDbFixtures.res +0 -1
  197. package/tests/querydb/QueryDbOperationsTest.res +99 -64
  198. package/tests/querydb/TaggedUnionStampTest.res +10 -8
  199. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
  200. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
  201. package/tests/util/CommandPublisherTest.res +0 -1
  202. package/tests/util/CsvStreamTest.res +3 -3
  203. 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("CreateItem on empty log succeeds", async () => {
70
- let results = await TestHandler.handleCommands(
71
- testDcbEventLog,
72
- Stream.fromIterable([
73
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
74
- ]),
75
- )->Effect.runPromise
76
-
77
- expect((
78
- results,
79
- mock.getEvents()->Array.length,
80
- mock.publishedEvents.contents->Array.length,
81
- ))->toEqual(([Ok("ref-1")], 1, 1))
82
- })
83
-
84
- testPromise("stored event carries only its entity tags (no framework provenance tag)", async () => {
85
- let _ = await TestHandler.handleCommands(
86
- testDcbEventLog,
87
- Stream.fromIterable([
88
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
89
- ]),
90
- )->Effect.runPromise
91
- let storedEvent = mock.getEvents()->Array.getUnsafe(0)
92
-
93
- // No `originatorSlice` (or any other) metadata tag is smuggled into the tag
94
- // set — a stored event's tags are exactly its DCB entity tags, so the
95
- // composite read/write keys stay aligned. See
96
- // docs/analysis/dcb-event-provenance-and-metadata.md.
97
- expect(storedEvent.tags)->toEqual([{Reventless.DcbTag.key: "itemId", value: "item-1"}])
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("NoOp returns Ok without storing events", async () => {
103
- let results = await TestHandler.handleCommands(
104
- testDcbEventLog,
105
- Stream.fromIterable([makeTopicItem("ref-noop", DcbFixtures.TestCommandSpec.NoOp)]),
106
- )->Effect.runPromise
107
-
108
- expect((results, mock.getEvents()->Array.length))->toEqual(([Ok("ref-noop")], 0))
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("CreateItem when item exists returns Ok (business-rule violations ACK, not NACK)", async () => {
114
- // First create the item
115
- let _ = await TestHandler.handleCommands(
116
- testDcbEventLog,
117
- Stream.fromIterable([
118
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
119
- ]),
120
- )->Effect.runPromise
121
- // Try to create again — decide returns Error, but handler returns Ok("rejected") to avoid infinite SQS retry
122
- let results = await TestHandler.handleCommands(
123
- testDcbEventLog,
124
- Stream.fromIterable([
125
- makeTopicItem("ref-2", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Duplicate"})),
126
- ]),
127
- )->Effect.runPromise
128
-
129
- expect(results)->toEqual([Ok("ref-2")])
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("retries and succeeds after 1 append failure", async () => {
135
- mock.failNextAppends := 1
136
- let results = await TestHandler.handleCommands(
137
- testDcbEventLog,
138
- Stream.fromIterable([
139
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
140
- ]),
141
- )->Effect.runPromise
142
-
143
- expect((results, mock.getEvents()->Array.length))->toEqual(([Ok("ref-1")], 1))
144
- })
145
-
146
- testPromise("returns Error after retries exhausted (4 failures)", async () => {
147
- mock.failNextAppends := 4
148
- let results = await TestHandler.handleCommands(
149
- testDcbEventLog,
150
- Stream.fromIterable([
151
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
152
- ]),
153
- )->Effect.runPromise
154
-
155
- expect((results, mock.getEvents()->Array.length))->toEqual(([Error("ref-1")], 0))
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("a conflict-free command reads eventually-consistent (no strong read)", async () => {
161
- let _ = await TestHandler.handleCommands(
162
- testDcbEventLog,
163
- Stream.fromIterable([
164
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
165
- ]),
166
- )->Effect.runPromise
167
-
168
- // Single attempt → single read, eventual.
169
- expect(mock.readStrongConsistency.contents)->toEqual([false])
170
- })
171
-
172
- testPromise("escalates to strong consistency on retry after a conflict", async () => {
173
- mock.failNextAppends := 1
174
- let _ = await TestHandler.handleCommands(
175
- testDcbEventLog,
176
- Stream.fromIterable([
177
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
178
- ]),
179
- )->Effect.runPromise
180
-
181
- // First attempt eventual, retry strong.
182
- expect(mock.readStrongConsistency.contents)->toEqual([false, true])
183
- })
184
-
185
- testPromise("every retry after the first is strong (2 failures)", async () => {
186
- mock.failNextAppends := 2
187
- let _ = await TestHandler.handleCommands(
188
- testDcbEventLog,
189
- Stream.fromIterable([
190
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
191
- ]),
192
- )->Effect.runPromise
193
-
194
- expect(mock.readStrongConsistency.contents)->toEqual([false, true, true])
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("AlwaysStrong reads strong on the first attempt", async () => {
200
- let _ = await StrongHandler.handleCommands(
201
- testDcbEventLog,
202
- Stream.fromIterable([
203
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
204
- ]),
205
- )->Effect.runPromise
206
-
207
- // No escalation step — strong from the start.
208
- expect(mock.readStrongConsistency.contents)->toEqual([true])
209
- })
210
-
211
- testPromise("AlwaysStrong stays strong across retries (2 failures)", async () => {
212
- mock.failNextAppends := 2
213
- let _ = await StrongHandler.handleCommands(
214
- testDcbEventLog,
215
- Stream.fromIterable([
216
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
217
- ]),
218
- )->Effect.runPromise
219
-
220
- expect(mock.readStrongConsistency.contents)->toEqual([true, true, true])
221
- })
222
-
223
- testPromise("AlwaysEventual never reads strong, even on retry (2 failures)", async () => {
224
- mock.failNextAppends := 2
225
- let _ = await EventualHandler.handleCommands(
226
- testDcbEventLog,
227
- Stream.fromIterable([
228
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
229
- ]),
230
- )->Effect.runPromise
231
-
232
- expect(mock.readStrongConsistency.contents)->toEqual([false, false, false])
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("RenameItem after CreateItem uses headPosition in condition", async () => {
238
- let _ = await TestHandler.handleCommands(
239
- testDcbEventLog,
240
- Stream.fromIterable([
241
- makeTopicItem("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"})),
242
- ]),
243
- )->Effect.runPromise
244
- let results = await TestHandler.handleCommands(
245
- testDcbEventLog,
246
- Stream.fromIterable([
247
- makeTopicItem(
248
- "ref-2",
249
- DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "Updated"}),
250
- ),
251
- ]),
252
- )->Effect.runPromise
253
-
254
- expect((results, mock.getEvents()->Array.length))->toEqual(([Ok("ref-2")], 2))
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
- describe("handleCommands - batch", () => {
259
- testPromise("multiple successful commands", async () => {
260
- let results = await TestHandler.handleCommands(
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 - projection cache", () => {
305
- // The decision-model projection cache (StateChangeSlice_Callback) caches the
306
- // `(decisionState, readHead)` per query. A warm same-entity command then reads
307
- // only events after `readHead` — observable here via `mock.readAfters`
308
- // (Some(_) = delta read, None = full read).
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([makeTopicItem(reference, command)]),
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
- let _ = await run("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "A"}))
317
- let _ = await run("ref-2", DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "B"}))
318
- let r3 = await run("ref-3", DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "C"}))
319
-
320
- // Read 1 (create, empty log) and read 2 (rename, head was None) are full
321
- // reads; read 3 hits the cache primed by read 2 and reads after position "1".
322
- expect((r3, mock.readAfters.contents, mock.getEvents()->Array.length))->toEqual((
323
- [Ok("ref-3")],
324
- [None, None, Some("1")],
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([makeTopicItem(reference, command)]),
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
- let _ = await run("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "A"}))
337
- let _ = await run("ref-2", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-2", name: "B"}))
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("conflict re-seeds the cache so the retry reads the delta", async () => {
344
- let run = (reference, command) =>
345
- TestHandler.handleCommands(
390
+ testPromise(
391
+ "empty batch returns empty array",
392
+ async () => {
393
+ let results = await TestHandler.handleCommands(
346
394
  testDcbEventLog,
347
- Stream.fromIterable([makeTopicItem(reference, command)]),
395
+ Stream.fromIterable([]),
348
396
  )->Effect.runPromise
349
397
 
350
- let _ = await run("ref-1", DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "A"}))
351
- // Force one append failure: the retry must re-seed from the just-read
352
- // snapshot and read only the delta after position "1".
353
- mock.failNextAppends := 1
354
- let r2 = await run("ref-2", DcbFixtures.TestCommandSpec.RenameItem({itemId: "item-1", newName: "B"}))
355
-
356
- expect((r2, mock.readAfters.contents, mock.getEvents()->Array.length))->toEqual((
357
- [Ok("ref-2")],
358
- [None, None, Some("1")],
359
- 2,
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
  })