@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.217

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 (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +6 -6
  3. package/src/ResourceAttribution.res +60 -4
  4. package/src/ResourceAttribution.res.mjs +26 -0
  5. package/src/adapter/QueryInterception/QueryInterception.res +74 -0
  6. package/src/adapter/QueryInterception/QueryInterception.res.mjs +26 -0
  7. package/src/adapter/ResourceAttribution_Deploytime.res +43 -0
  8. package/src/adapter/ResourceAttribution_Deploytime.res.mjs +20 -0
  9. package/src/admin/Platform_Admin_Structure.res +4 -0
  10. package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
  11. package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
  12. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
  13. package/src/components/Aggregate/Aggregate_Callback.res +14 -2
  14. package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
  15. package/src/components/Builder_Helpers.res +21 -3
  16. package/src/components/Builder_Helpers.res.mjs +4 -3
  17. package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
  18. package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
  19. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
  20. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +4 -1
  21. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +2 -1
  22. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
  23. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  24. package/src/plugin/component/Plugin_Builder.res +6 -1
  25. package/src/plugin/component/Plugin_Builder.res.mjs +4 -2
  26. package/src/plugin/component/Plugin_Helpers.res +7 -2
  27. package/src/plugin/component/Plugin_Helpers.res.mjs +4 -3
  28. package/src/plugin/component/Plugin_Structure.res +66 -51
  29. package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
  30. package/tests/ResourceAttributionTest.res +115 -0
  31. package/tests/ResourceAttributionTest.res.mjs +106 -0
  32. package/tests/adapter/QueryInterceptionTest.res +55 -0
  33. package/tests/adapter/QueryInterceptionTest.res.mjs +49 -0
  34. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
  35. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
  36. package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
  37. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
  38. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
  39. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
  40. package/tests/message/MessageTest.res +1 -0
  41. package/tests/message/MessageTest.res.mjs +1 -0
  42. package/tests/plugin/PluginStructureTest.res +27 -0
  43. package/tests/plugin/PluginStructureTest.res.mjs +27 -0
  44. package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
  45. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
  46. package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
@@ -0,0 +1,258 @@
1
+ // The command-outcome hook, at the property the side-channels don't have: it fires on the
2
+ // fire-and-forget dispatch path — no acceptedResultChannel/rejectedResultChannel is installed
3
+ // anywhere in this file — for both write-side component kinds, carrying the owning component
4
+ // and telling a domain rejection apart from an infrastructure failure.
5
+
6
+ open JestGlobals
7
+
8
+ // ── StateChangeSlice arm (the aggregate arm reuses AggregateFixtures as-is) ──
9
+
10
+ let dcbMock = DcbFixtures.makeMockStorage()
11
+
12
+ module TestDcbOps: DcbEventLog_Operations.Ops = {
13
+ let name = "TestDcbEventLog"
14
+ let serviceName = "TestDcbEventLog"
15
+ let storage = dcbMock.operations
16
+ let publishJson = dcbMock.mockPublishJson
17
+ }
18
+
19
+ module EventLogOps = DcbEventLog_Operations.Make(TestDcbOps)
20
+
21
+ let dcbEventLog: DcbEventLog.operations = {
22
+ read: EventLogOps.read,
23
+ append: EventLogOps.append,
24
+ readStream: EventLogOps.readStream,
25
+ appendStream: EventLogOps.appendStream,
26
+ }
27
+
28
+ module SliceBehavior = {
29
+ type state = DcbFixtures.TestCommandSpec.state
30
+ let initialState = DcbFixtures.TestCommandSpec.initialState
31
+ let evolve = DcbFixtures.TestCommandSpec.evolve
32
+ let decide = DcbFixtures.TestCommandSpec.decide
33
+ let moduleUrl = DcbFixtures.TestCommandSpec.moduleUrl
34
+ }
35
+
36
+ module SliceHandler = StateChangeSlice_Callback.Make(DcbFixtures.TestCommandSpec, SliceBehavior)
37
+
38
+ let sliceItem = (reference, command): CommandTopic.topicItem<
39
+ Message.command'<Reventless.Id.String.t, DcbFixtures.TestCommandSpec.command>,
40
+ > => {
41
+ command: {
42
+ id: Reventless.Id.String.makeFromString("cmd-" ++ reference),
43
+ meta: DcbFixtures.testMeta,
44
+ command,
45
+ },
46
+ reference,
47
+ }
48
+
49
+ let runSlice = (reference, command) =>
50
+ SliceHandler.handleCommands(dcbEventLog, Stream.fromIterable([sliceItem(reference, command)]))
51
+ ->Effect.runPromise
52
+
53
+ let runAgg = (~aggId="agg-1", reference, command) =>
54
+ Stream.fromIterable([AggregateFixtures.makeTopicItem(~aggId, reference, command)])
55
+ ->AggregateFixtures.TestHandler.handleCommands
56
+ ->Effect.runPromise
57
+
58
+ // ── Capture ──
59
+
60
+ let observed: ref<array<CommandTopic_Helpers.commandOutcomeReport>> = ref([])
61
+
62
+ // Flattened for readable assertions: (component, reference, disposition, errorCode, eventCount).
63
+ let seen = () =>
64
+ observed.contents->Array.map(r =>
65
+ switch r.outcome {
66
+ | OutcomeAccepted({eventCount}) => (r.component, r.reference, "accepted", "", eventCount)
67
+ | OutcomeRejected({errorCode, cause}) => (
68
+ r.component,
69
+ r.reference,
70
+ switch cause {
71
+ | DomainRejection => "rejected/domain"
72
+ | InfrastructureFailure => "rejected/infrastructure"
73
+ },
74
+ errorCode,
75
+ 0,
76
+ )
77
+ }
78
+ )
79
+
80
+ let record = () =>
81
+ CommandTopic_Helpers.registerCommandOutcome(r =>
82
+ observed := observed.contents->Array.concat([r])
83
+ )
84
+
85
+ let _ = beforeEach(() => {
86
+ AggregateFixtures.mock.reset()
87
+ AggregateFixtures.TestHandler.resetCache()
88
+ dcbMock.reset()
89
+ SliceHandler.resetCache()
90
+ observed := []
91
+ CommandTopic_Helpers.clearCommandOutcome()
92
+ RuntimeExtension.reset()
93
+ })
94
+
95
+ describe("CommandTopic command-outcome hook:", () => {
96
+ describe("aggregate", () => {
97
+ testPromise("observes an accepted command with no side-channel installed", async () => {
98
+ record()
99
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
100
+
101
+ expect(results)->toEqual([Ok("ref-1")])
102
+ expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
103
+ })
104
+
105
+ testPromise("carries the entity id of an accepted command", async () => {
106
+ record()
107
+ let _ = await runAgg(~aggId="agg-7", "ref-1", AggregateFixtures.AggSpec.Create({name: "W"}))
108
+
109
+ let entityId = switch (observed.contents->Array.getUnsafe(0)).outcome {
110
+ | OutcomeAccepted({entityId}) => entityId
111
+ | OutcomeRejected(_) => None
112
+ }
113
+ expect(entityId)->toEqual(Some("agg-7"))
114
+ })
115
+
116
+ testPromise("reports a decide rejection as domain, with the declared error code", async () => {
117
+ // Seed before registering, so only the second (rejected) Create is observed.
118
+ let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
119
+ record()
120
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
121
+
122
+ expect(results)->toEqual([Ok("ref-1")])
123
+ expect(seen())->toEqual([
124
+ ("TestAggregate", "ref-1", "rejected/domain", "AlreadyExists", 0),
125
+ ])
126
+ })
127
+
128
+ testPromise("reports a failed append as infrastructure, not a domain rejection", async () => {
129
+ record()
130
+ AggregateFixtures.mock.failNextAppend := true
131
+ let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
132
+
133
+ // `decide` said Ok here — the store is what failed, and the hook has to say so.
134
+ expect(seen())->toEqual([
135
+ ("TestAggregate", "ref-1", "rejected/infrastructure", "AppendFailed", 0),
136
+ ])
137
+ })
138
+
139
+ testPromise("observes every command of a mixed batch", async () => {
140
+ let _ = await runAgg("ref-seed", AggregateFixtures.AggSpec.Create({name: "Widget"}))
141
+ record()
142
+ let _ =
143
+ await Stream.fromIterable([
144
+ AggregateFixtures.makeTopicItem("ref-1", AggregateFixtures.AggSpec.Rename({newName: "A"})),
145
+ AggregateFixtures.makeTopicItem("ref-2", AggregateFixtures.AggSpec.Create({name: "B"})),
146
+ AggregateFixtures.makeTopicItem("ref-3", AggregateFixtures.AggSpec.Rename({newName: "C"})),
147
+ ])
148
+ ->AggregateFixtures.TestHandler.handleCommands
149
+ ->Effect.runPromise
150
+
151
+ expect(seen())->toEqual([
152
+ ("TestAggregate", "ref-1", "accepted", "", 2),
153
+ ("TestAggregate", "ref-2", "rejected/domain", "AlreadyExists", 0),
154
+ ("TestAggregate", "ref-3", "accepted", "", 2),
155
+ ])
156
+ })
157
+ })
158
+
159
+ describe("state-change slice", () => {
160
+ testPromise("observes an accepted command with no side-channel installed", async () => {
161
+ record()
162
+ let _ = await runSlice(
163
+ "ref-1",
164
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
165
+ )
166
+
167
+ expect(seen())->toEqual([
168
+ ("TestStateChangeSlice", DcbFixtures.testMeta.msgId, "accepted", "", 1),
169
+ ])
170
+ })
171
+
172
+ testPromise("reports a decide rejection as domain, with the declared error code", async () => {
173
+ let _ = await runSlice(
174
+ "ref-seed",
175
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
176
+ )
177
+ record()
178
+ let _ = await runSlice(
179
+ "ref-1",
180
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Again"}),
181
+ )
182
+
183
+ expect(seen())->toEqual([
184
+ (
185
+ "TestStateChangeSlice",
186
+ DcbFixtures.testMeta.msgId,
187
+ "rejected/domain",
188
+ "ItemAlreadyExists",
189
+ 0,
190
+ ),
191
+ ])
192
+ })
193
+
194
+ testPromise("reports exhausted append conflicts as infrastructure", async () => {
195
+ record()
196
+ // One more failure than the retry loop can absorb.
197
+ dcbMock.failNextAppends := 4
198
+ let _ = await runSlice(
199
+ "ref-1",
200
+ DcbFixtures.TestCommandSpec.CreateItem({itemId: "item-1", name: "Test"}),
201
+ )
202
+
203
+ expect(seen())->toEqual([
204
+ ("TestStateChangeSlice", DcbFixtures.testMeta.msgId, "rejected/infrastructure", "Conflict", 0),
205
+ ])
206
+ })
207
+ })
208
+
209
+ describe("registration", () => {
210
+ testPromise("with nothing registered nothing is observed and the command is unaffected", async () => {
211
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
212
+
213
+ expect(results)->toEqual([Ok("ref-1")])
214
+ expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
215
+ expect(observed.contents->Array.length)->toBe(0)
216
+ })
217
+
218
+ testPromise("clearCommandOutcome stops observation", async () => {
219
+ record()
220
+ CommandTopic_Helpers.clearCommandOutcome()
221
+ let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
222
+
223
+ expect(observed.contents->Array.length)->toBe(0)
224
+ })
225
+
226
+ testPromise("an out-of-tree extension can register it from onColdStart", async () => {
227
+ // The hook needs no registration path of its own: it has the shape the cold-start
228
+ // seam already reaches, so an extension picks it up alongside the other four.
229
+ module Accountant: RuntimeExtension.Extension = {
230
+ let moduleUrl = "file:///pkg/accountant.res.mjs"
231
+ let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) =>
232
+ record()
233
+ }
234
+ RuntimeExtension.use(module(Accountant: RuntimeExtension.Extension))
235
+ RuntimeExtension.notifyColdStart(
236
+ ~runtimeKind=ComponentType.Aggregate,
237
+ ~component="AllAggregates",
238
+ ~plugin=Some("Catalog"),
239
+ ~platform=Some("Shop"),
240
+ )
241
+
242
+ let _ = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
243
+
244
+ expect(seen())->toEqual([("TestAggregate", "ref-1", "accepted", "", 1)])
245
+ })
246
+
247
+ testPromise("a throwing hook is swallowed and the command still succeeds", async () => {
248
+ CommandTopic_Helpers.registerCommandOutcome(_ =>
249
+ JsError.throwWithMessage("hook exploded")
250
+ )
251
+ let results = await runAgg("ref-1", AggregateFixtures.AggSpec.Create({name: "Widget"}))
252
+
253
+ // The outcome the hook observes has already happened — an observer must not undo it.
254
+ expect(results)->toEqual([Ok("ref-1")])
255
+ expect(AggregateFixtures.mock.getAll()->Array.length)->toBe(1)
256
+ })
257
+ })
258
+ })
@@ -0,0 +1,372 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Stream from "effect/Stream";
6
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as DcbFixtures$ReventlessCore from "../dcb/DcbFixtures.res.mjs";
8
+ import * as RuntimeExtension$ReventlessCore from "../../src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
9
+ import * as AggregateFixtures$ReventlessCore from "../aggregate/AggregateFixtures.res.mjs";
10
+ import * as CommandTopic_Helpers$ReventlessCore from "../../src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
11
+ import * as DcbEventLog_Operations$ReventlessCore from "../../src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
12
+ import * as StateChangeSlice_Callback$ReventlessCore from "../../src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
13
+
14
+ let dcbMock = DcbFixtures$ReventlessCore.makeMockStorage();
15
+
16
+ let storage = dcbMock.operations;
17
+
18
+ let publishJson = dcbMock.mockPublishJson;
19
+
20
+ let TestDcbOps = {
21
+ name: "TestDcbEventLog",
22
+ serviceName: "TestDcbEventLog",
23
+ storage: storage,
24
+ publishJson: publishJson
25
+ };
26
+
27
+ let EventLogOps = DcbEventLog_Operations$ReventlessCore.Make(TestDcbOps);
28
+
29
+ let dcbEventLog_read = EventLogOps.read;
30
+
31
+ let dcbEventLog_append = EventLogOps.append;
32
+
33
+ let dcbEventLog_readStream = EventLogOps.readStream;
34
+
35
+ let dcbEventLog_appendStream = EventLogOps.appendStream;
36
+
37
+ let dcbEventLog = {
38
+ read: dcbEventLog_read,
39
+ append: dcbEventLog_append,
40
+ readStream: dcbEventLog_readStream,
41
+ appendStream: dcbEventLog_appendStream
42
+ };
43
+
44
+ let initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
45
+
46
+ let evolve = DcbFixtures$ReventlessCore.TestCommandSpec.evolve;
47
+
48
+ let decide = DcbFixtures$ReventlessCore.TestCommandSpec.decide;
49
+
50
+ let moduleUrl = DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl;
51
+
52
+ let SliceBehavior = {
53
+ initialState: initialState,
54
+ evolve: evolve,
55
+ decide: decide,
56
+ moduleUrl: moduleUrl
57
+ };
58
+
59
+ let SliceHandler = StateChangeSlice_Callback$ReventlessCore.Make({
60
+ name: DcbFixtures$ReventlessCore.TestCommandSpec.name,
61
+ moduleUrl: DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl,
62
+ Id: Id$Reventless.$$String,
63
+ consumedEventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.consumedEventSchema,
64
+ errorSchema: DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema,
65
+ eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
66
+ commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
67
+ commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
68
+ readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
69
+ })(SliceBehavior);
70
+
71
+ function sliceItem(reference, command) {
72
+ return {
73
+ command: {
74
+ id: Id$Reventless.$$String.makeFromString("cmd-" + reference),
75
+ meta: DcbFixtures$ReventlessCore.testMeta,
76
+ command: command
77
+ },
78
+ reference: reference
79
+ };
80
+ }
81
+
82
+ function runSlice(reference, command) {
83
+ return Effect.runPromise(SliceHandler.handleCommands(undefined, undefined, dcbEventLog, Stream.fromIterable([sliceItem(reference, command)])));
84
+ }
85
+
86
+ function runAgg(aggIdOpt, reference, command) {
87
+ let aggId = aggIdOpt !== undefined ? aggIdOpt : "agg-1";
88
+ return Effect.runPromise(AggregateFixtures$ReventlessCore.TestHandler.handleCommands(Stream.fromIterable([AggregateFixtures$ReventlessCore.makeTopicItem(aggId, reference, command)])));
89
+ }
90
+
91
+ let observed = {
92
+ contents: []
93
+ };
94
+
95
+ function seen() {
96
+ return observed.contents.map(r => {
97
+ let match = r.outcome;
98
+ if (match.TAG === "OutcomeAccepted") {
99
+ return [
100
+ r.component,
101
+ r.reference,
102
+ "accepted",
103
+ "",
104
+ match.eventCount
105
+ ];
106
+ }
107
+ let tmp;
108
+ tmp = match.cause === "DomainRejection" ? "rejected/domain" : "rejected/infrastructure";
109
+ return [
110
+ r.component,
111
+ r.reference,
112
+ tmp,
113
+ match.errorCode,
114
+ 0
115
+ ];
116
+ });
117
+ }
118
+
119
+ function record() {
120
+ CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(r => {
121
+ observed.contents = observed.contents.concat([r]);
122
+ });
123
+ }
124
+
125
+ globalThis.beforeEach(() => {
126
+ AggregateFixtures$ReventlessCore.mock.reset();
127
+ AggregateFixtures$ReventlessCore.TestHandler.resetCache();
128
+ dcbMock.reset();
129
+ SliceHandler.resetCache();
130
+ observed.contents = [];
131
+ CommandTopic_Helpers$ReventlessCore.clearCommandOutcome();
132
+ RuntimeExtension$ReventlessCore.reset();
133
+ });
134
+
135
+ globalThis.describe("CommandTopic command-outcome hook:", () => {
136
+ globalThis.describe("aggregate", () => {
137
+ globalThis.test("observes an accepted command with no side-channel installed", async () => {
138
+ record();
139
+ let results = await runAgg(undefined, "ref-1", {
140
+ TAG: "Create",
141
+ name: "Widget"
142
+ });
143
+ globalThis.expect(results).toEqual([{
144
+ TAG: "Ok",
145
+ _0: "ref-1"
146
+ }]);
147
+ globalThis.expect(seen()).toEqual([[
148
+ "TestAggregate",
149
+ "ref-1",
150
+ "accepted",
151
+ "",
152
+ 1
153
+ ]]);
154
+ });
155
+ globalThis.test("carries the entity id of an accepted command", async () => {
156
+ record();
157
+ await runAgg("agg-7", "ref-1", {
158
+ TAG: "Create",
159
+ name: "W"
160
+ });
161
+ let match = observed.contents[0].outcome;
162
+ let entityId;
163
+ entityId = match.TAG === "OutcomeAccepted" ? match.entityId : undefined;
164
+ globalThis.expect(entityId).toEqual("agg-7");
165
+ });
166
+ globalThis.test("reports a decide rejection as domain, with the declared error code", async () => {
167
+ await runAgg(undefined, "ref-seed", {
168
+ TAG: "Create",
169
+ name: "Widget"
170
+ });
171
+ record();
172
+ let results = await runAgg(undefined, "ref-1", {
173
+ TAG: "Create",
174
+ name: "Widget"
175
+ });
176
+ globalThis.expect(results).toEqual([{
177
+ TAG: "Ok",
178
+ _0: "ref-1"
179
+ }]);
180
+ globalThis.expect(seen()).toEqual([[
181
+ "TestAggregate",
182
+ "ref-1",
183
+ "rejected/domain",
184
+ "AlreadyExists",
185
+ 0
186
+ ]]);
187
+ });
188
+ globalThis.test("reports a failed append as infrastructure, not a domain rejection", async () => {
189
+ record();
190
+ AggregateFixtures$ReventlessCore.mock.failNextAppend.contents = true;
191
+ await runAgg(undefined, "ref-1", {
192
+ TAG: "Create",
193
+ name: "Widget"
194
+ });
195
+ globalThis.expect(seen()).toEqual([[
196
+ "TestAggregate",
197
+ "ref-1",
198
+ "rejected/infrastructure",
199
+ "AppendFailed",
200
+ 0
201
+ ]]);
202
+ });
203
+ globalThis.test("observes every command of a mixed batch", async () => {
204
+ await runAgg(undefined, "ref-seed", {
205
+ TAG: "Create",
206
+ name: "Widget"
207
+ });
208
+ record();
209
+ await Effect.runPromise(AggregateFixtures$ReventlessCore.TestHandler.handleCommands(Stream.fromIterable([
210
+ AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-1", {
211
+ TAG: "Rename",
212
+ newName: "A"
213
+ }),
214
+ AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-2", {
215
+ TAG: "Create",
216
+ name: "B"
217
+ }),
218
+ AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-3", {
219
+ TAG: "Rename",
220
+ newName: "C"
221
+ })
222
+ ])));
223
+ globalThis.expect(seen()).toEqual([
224
+ [
225
+ "TestAggregate",
226
+ "ref-1",
227
+ "accepted",
228
+ "",
229
+ 2
230
+ ],
231
+ [
232
+ "TestAggregate",
233
+ "ref-2",
234
+ "rejected/domain",
235
+ "AlreadyExists",
236
+ 0
237
+ ],
238
+ [
239
+ "TestAggregate",
240
+ "ref-3",
241
+ "accepted",
242
+ "",
243
+ 2
244
+ ]
245
+ ]);
246
+ });
247
+ });
248
+ globalThis.describe("state-change slice", () => {
249
+ globalThis.test("observes an accepted command with no side-channel installed", async () => {
250
+ record();
251
+ await runSlice("ref-1", {
252
+ TAG: "CreateItem",
253
+ itemId: "item-1",
254
+ name: "Test"
255
+ });
256
+ globalThis.expect(seen()).toEqual([[
257
+ "TestStateChangeSlice",
258
+ DcbFixtures$ReventlessCore.testMeta.msgId,
259
+ "accepted",
260
+ "",
261
+ 1
262
+ ]]);
263
+ });
264
+ globalThis.test("reports a decide rejection as domain, with the declared error code", async () => {
265
+ await runSlice("ref-seed", {
266
+ TAG: "CreateItem",
267
+ itemId: "item-1",
268
+ name: "Test"
269
+ });
270
+ record();
271
+ await runSlice("ref-1", {
272
+ TAG: "CreateItem",
273
+ itemId: "item-1",
274
+ name: "Again"
275
+ });
276
+ globalThis.expect(seen()).toEqual([[
277
+ "TestStateChangeSlice",
278
+ DcbFixtures$ReventlessCore.testMeta.msgId,
279
+ "rejected/domain",
280
+ "ItemAlreadyExists",
281
+ 0
282
+ ]]);
283
+ });
284
+ globalThis.test("reports exhausted append conflicts as infrastructure", async () => {
285
+ record();
286
+ dcbMock.failNextAppends.contents = 4;
287
+ await runSlice("ref-1", {
288
+ TAG: "CreateItem",
289
+ itemId: "item-1",
290
+ name: "Test"
291
+ });
292
+ globalThis.expect(seen()).toEqual([[
293
+ "TestStateChangeSlice",
294
+ DcbFixtures$ReventlessCore.testMeta.msgId,
295
+ "rejected/infrastructure",
296
+ "Conflict",
297
+ 0
298
+ ]]);
299
+ });
300
+ });
301
+ globalThis.describe("registration", () => {
302
+ globalThis.test("with nothing registered nothing is observed and the command is unaffected", async () => {
303
+ let results = await runAgg(undefined, "ref-1", {
304
+ TAG: "Create",
305
+ name: "Widget"
306
+ });
307
+ globalThis.expect(results).toEqual([{
308
+ TAG: "Ok",
309
+ _0: "ref-1"
310
+ }]);
311
+ globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
312
+ globalThis.expect(observed.contents.length).toBe(0);
313
+ });
314
+ globalThis.test("clearCommandOutcome stops observation", async () => {
315
+ record();
316
+ CommandTopic_Helpers$ReventlessCore.clearCommandOutcome();
317
+ await runAgg(undefined, "ref-1", {
318
+ TAG: "Create",
319
+ name: "Widget"
320
+ });
321
+ globalThis.expect(observed.contents.length).toBe(0);
322
+ });
323
+ globalThis.test("an out-of-tree extension can register it from onColdStart", async () => {
324
+ let onColdStart = (param, param$1, param$2, param$3) => record();
325
+ let Accountant = {
326
+ moduleUrl: "file:///pkg/accountant.res.mjs",
327
+ onColdStart: onColdStart
328
+ };
329
+ RuntimeExtension$ReventlessCore.use(Accountant);
330
+ RuntimeExtension$ReventlessCore.notifyColdStart("Aggregate", "AllAggregates", "Catalog", "Shop");
331
+ await runAgg(undefined, "ref-1", {
332
+ TAG: "Create",
333
+ name: "Widget"
334
+ });
335
+ globalThis.expect(seen()).toEqual([[
336
+ "TestAggregate",
337
+ "ref-1",
338
+ "accepted",
339
+ "",
340
+ 1
341
+ ]]);
342
+ });
343
+ globalThis.test("a throwing hook is swallowed and the command still succeeds", async () => {
344
+ CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(param => Stdlib_JsError.throwWithMessage("hook exploded"));
345
+ let results = await runAgg(undefined, "ref-1", {
346
+ TAG: "Create",
347
+ name: "Widget"
348
+ });
349
+ globalThis.expect(results).toEqual([{
350
+ TAG: "Ok",
351
+ _0: "ref-1"
352
+ }]);
353
+ globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
354
+ });
355
+ });
356
+ });
357
+
358
+ export {
359
+ dcbMock,
360
+ TestDcbOps,
361
+ EventLogOps,
362
+ dcbEventLog,
363
+ SliceBehavior,
364
+ SliceHandler,
365
+ sliceItem,
366
+ runSlice,
367
+ runAgg,
368
+ observed,
369
+ seen,
370
+ record,
371
+ }
372
+ /* dcbMock Not a pure module */
@@ -26,8 +26,14 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
26
26
  ->Effect.map(items => {
27
27
  items->Array.forEach(item => {
28
28
  // Both channels report for the same reference; rejected wins.
29
- CommandTopic_Helpers.reportAccepted(item.reference, {entityId: "agg-1", eventCount: 0})
29
+ CommandTopic_Helpers.reportAccepted(
30
+ ~component="TestService",
31
+ item.reference,
32
+ {entityId: "agg-1", eventCount: 0},
33
+ )
30
34
  CommandTopic_Helpers.reportRejected(
35
+ ~component="TestService",
36
+ ~cause=DomainRejection,
31
37
  item.reference,
32
38
  {errorCode: "AlreadyExists", errorDetail: ""},
33
39
  )
@@ -53,6 +59,8 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
53
59
  ->Effect.map(items => {
54
60
  items->Array.forEach(item =>
55
61
  CommandTopic_Helpers.reportRejected(
62
+ ~component="TestService",
63
+ ~cause=DomainRejection,
56
64
  item.reference,
57
65
  {errorCode: "BusinessRuleViolated", errorDetail: "{\"reason\":\"x\"}"},
58
66
  )
@@ -30,11 +30,11 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
30
30
  globalThis.test("Rejected from rejectedResultChannel beats Accepted from acceptedResultChannel", async () => {
31
31
  let handler = stream => Effect.map(Stream.runCollect(stream), items => {
32
32
  items.forEach(item => {
33
- CommandTopic_Helpers$ReventlessCore.reportAccepted(item.reference, {
33
+ CommandTopic_Helpers$ReventlessCore.reportAccepted("TestService", item.reference, {
34
34
  entityId: "agg-1",
35
35
  eventCount: 0
36
36
  });
37
- CommandTopic_Helpers$ReventlessCore.reportRejected(item.reference, {
37
+ CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "DomainRejection", item.reference, {
38
38
  errorCode: "AlreadyExists",
39
39
  errorDetail: ""
40
40
  });
@@ -61,7 +61,7 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
61
61
  });
62
62
  globalThis.test("Rejected from rejectedResultChannel beats Error result (synthesized Conflict)", async () => {
63
63
  let handler = stream => Effect.map(Stream.runCollect(stream), items => {
64
- items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected(item.reference, {
64
+ items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "DomainRejection", item.reference, {
65
65
  errorCode: "BusinessRuleViolated",
66
66
  errorDetail: "{\"reason\":\"x\"}"
67
67
  }));
@@ -122,6 +122,7 @@ describe("Message should", () => {
122
122
  linkedViews: [],
123
123
  consistencyRead: None,
124
124
  events: [],
125
+ errors: [],
125
126
  chapter: None,
126
127
  },
127
128
  ],
@@ -114,6 +114,7 @@ globalThis.describe("Message should", () => {
114
114
  linkedViews: [],
115
115
  consistencyRead: undefined,
116
116
  events: [],
117
+ errors: [],
117
118
  chapter: undefined
118
119
  }],
119
120
  automationSlices: [],