@reventlessdev/reventless-local 3.0.0-alpha.301 → 3.0.0-alpha.303

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 (36) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +9 -9
  3. package/src/EmitCapabilities.res +12 -5
  4. package/src/EmitCapabilities.res.mjs +13 -1
  5. package/src/Platform.res +1 -2
  6. package/src/Platform.res.mjs +6 -10
  7. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +2 -2
  8. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +0 -2
  9. package/src/adapter/LocalCapabilities.res +1 -1
  10. package/src/adapter/LocalCapabilities.res.mjs +2 -1
  11. package/src/adapter/LocalFingerprintKey.res +66 -0
  12. package/src/adapter/LocalFingerprintKey.res.mjs +75 -0
  13. package/src/adapter/LocalUploadResolvers.res +7 -7
  14. package/src/adapter/LocalUploadResolvers.res.mjs +5 -4
  15. package/src/adapter/Scheduler/LocalScheduledPublisher.res +0 -1
  16. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +0 -1
  17. package/tests/EmitCapabilitiesTest.res +6 -0
  18. package/tests/EmitCapabilitiesTest.res.mjs +6 -0
  19. package/tests/PluginEventDecodeTest.res.mjs +2 -2
  20. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +1 -1
  21. package/tests/adapter/DcbEventLogStorageTest.res.mjs +1 -1
  22. package/tests/adapter/ObjectStorePersistenceTest.res +6 -3
  23. package/tests/adapter/ObjectStorePersistenceTest.res.mjs +3 -3
  24. package/tests/adapter/ServedBucketHttpTest.res +3 -1
  25. package/tests/adapter/ServedBucketHttpTest.res.mjs +2 -1
  26. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +1 -1
  27. package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +1 -1
  28. package/tests/components/dcb/DcbRepeatTest.res +291 -0
  29. package/tests/components/dcb/DcbRepeatTest.res.mjs +420 -0
  30. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +71 -15
  31. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +119 -14
  32. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res.mjs +1 -1
  33. package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +1 -1
  34. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +1 -1
  35. package/tests/plugin/Plugin_Fixtures.res +1 -3
  36. package/tests/plugin/Plugin_Fixtures.res.mjs +1 -2
@@ -0,0 +1,420 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
5
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Stream from "effect/Stream";
8
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
10
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
11
+ import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
12
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
13
+ import * as BackendState$ReventlessLocal from "../../../src/adapter/BackendState.res.mjs";
14
+ import * as SqliteDriver$ReventlessLocal from "../../../src/adapter/SqliteDriver.res.mjs";
15
+ import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
16
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
17
+ import * as DcbEventLog_Operations$ReventlessCore from "@reventlessdev/reventless-core/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
18
+ import * as LocalDcbEventLogStorage$ReventlessLocal from "../../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
19
+ import * as StateChangeSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
20
+
21
+ TestRunner$ReventlessLocal.setup();
22
+
23
+ let opts = {};
24
+
25
+ let name = "Ordering.Orders";
26
+
27
+ let commandSchema = Sury.literal("Noop");
28
+
29
+ let eventSchema = Sury.$schema(s => ({
30
+ TAG: "ItemOrdered",
31
+ productId: s.m(Sury.string),
32
+ orderId: s.m(Sury.string)
33
+ }));
34
+
35
+ let directiveSchema = Sury.literal("NoDirective");
36
+
37
+ let moduleUrl = import.meta.url;
38
+
39
+ function authorizationOf(param) {
40
+ return "AllowAuthenticated";
41
+ }
42
+
43
+ function commandTransition(param) {
44
+ return "Undeclared";
45
+ }
46
+
47
+ let traits = [];
48
+
49
+ let Port = {
50
+ name: name,
51
+ commandSchema: commandSchema,
52
+ eventSchema: eventSchema,
53
+ directiveSchema: directiveSchema,
54
+ moduleUrl: moduleUrl,
55
+ authorizationOf: authorizationOf,
56
+ commandTransition: commandTransition,
57
+ traits: traits
58
+ };
59
+
60
+ let name$1 = "RecordDemandNoCheck";
61
+
62
+ let moduleUrl$1 = import.meta.url;
63
+
64
+ let eventSchema$1 = Sury.$schema(s => ({
65
+ TAG: "DemandRecorded",
66
+ productId: s.m(DcbTag$Reventless.partition),
67
+ orderId: s.m(Sury.string)
68
+ }));
69
+
70
+ let consumedEventSchema = Sury.$schema(s => ({
71
+ TAG: "DemandRecorded",
72
+ productId: s.m(DcbTag$Reventless.string),
73
+ orderId: s.m(Sury.string)
74
+ }));
75
+
76
+ let commandSchema$1 = Sury.$schema(s => ({
77
+ TAG: "RecordDemand",
78
+ productId: s.m(DcbTag$Reventless.string),
79
+ orderId: s.m(Sury.string)
80
+ }));
81
+
82
+ let errorSchema = Sury.literal("Never");
83
+
84
+ function authorizationOf$1(param) {
85
+ return "AllowAuthenticated";
86
+ }
87
+
88
+ function commandTransition$1(param) {
89
+ return "Unrestricted";
90
+ }
91
+
92
+ let traits$1 = [];
93
+
94
+ let DemandSlice = {
95
+ name: name$1,
96
+ Id: undefined,
97
+ moduleUrl: moduleUrl$1,
98
+ eventSchema: eventSchema$1,
99
+ consumedEventSchema: consumedEventSchema,
100
+ commandSchema: commandSchema$1,
101
+ errorSchema: errorSchema,
102
+ authorizationOf: authorizationOf$1,
103
+ commandTransition: commandTransition$1,
104
+ traits: traits$1,
105
+ readConsistency: "EscalateOnRetry"
106
+ };
107
+
108
+ let initialState = {
109
+ orders: []
110
+ };
111
+
112
+ function evolve(state, event) {
113
+ return {
114
+ orders: state.orders.concat([event.orderId])
115
+ };
116
+ }
117
+
118
+ function decide(_state, command) {
119
+ return {
120
+ TAG: "Ok",
121
+ _0: [{
122
+ TAG: "DemandRecorded",
123
+ productId: command.productId,
124
+ orderId: command.orderId
125
+ }]
126
+ };
127
+ }
128
+
129
+ let DemandDecision = {
130
+ initialState: initialState,
131
+ moduleUrl: moduleUrl$1,
132
+ evolve: evolve,
133
+ decide: decide
134
+ };
135
+
136
+ let DemandHandler = StateChangeSlice_Callback$ReventlessCore.Make({
137
+ name: name$1,
138
+ moduleUrl: moduleUrl$1,
139
+ consumedEventSchema: consumedEventSchema,
140
+ errorSchema: errorSchema,
141
+ eventSchema: eventSchema$1,
142
+ commandSchema: commandSchema$1,
143
+ authorizationOf: authorizationOf$1,
144
+ commandTransition: commandTransition$1,
145
+ traits: traits$1,
146
+ readConsistency: "EscalateOnRetry"
147
+ })({
148
+ initialState: initialState,
149
+ evolve: evolve,
150
+ decide: decide,
151
+ moduleUrl: moduleUrl$1
152
+ });
153
+
154
+ let partitionTag = {
155
+ TAG: "Simple",
156
+ _0: {
157
+ key: "productId"
158
+ }
159
+ };
160
+
161
+ let moduleUrl$2 = import.meta.url;
162
+
163
+ function mapIncomingEvent(_id, event, _meta, _pluginDef, _queryEngine) {
164
+ return [{
165
+ TAG: "PublishStateChangeSliceCommand",
166
+ _0: {
167
+ TAG: "RecordDemand",
168
+ productId: event.productId,
169
+ orderId: event.orderId
170
+ }
171
+ }];
172
+ }
173
+
174
+ let handledEvents = [];
175
+
176
+ let issuedCommands = [];
177
+
178
+ let DemandExtension = ExtensionMapping$ReventlessInfra.Make({
179
+ ExtensionPoint: {
180
+ name: name,
181
+ moduleUrl: moduleUrl,
182
+ commandSchema: commandSchema,
183
+ eventSchema: eventSchema,
184
+ directiveSchema: directiveSchema
185
+ },
186
+ Delegate: {
187
+ Id: Id$Reventless.$$String,
188
+ name: name$1,
189
+ eventSchema: eventSchema$1,
190
+ errorSchema: errorSchema,
191
+ commandSchema: commandSchema$1,
192
+ moduleUrl: moduleUrl$1,
193
+ authorizationOf: authorizationOf$1,
194
+ commandTransition: commandTransition$1,
195
+ traits: traits$1
196
+ },
197
+ moduleUrl: moduleUrl$2,
198
+ delegateModuleUrl: moduleUrl$1,
199
+ mapIncomingEvent: mapIncomingEvent,
200
+ mapOutgoingEvent: undefined,
201
+ handledEvents: handledEvents,
202
+ issuedCommands: issuedCommands
203
+ });
204
+
205
+ async function queryEngine_scan(param, param$1, param$2) {
206
+ return [];
207
+ }
208
+
209
+ async function queryEngine_query(param, param$1, param$2, param$3, param$4, param$5, param$6) {
210
+ return [];
211
+ }
212
+
213
+ let queryEngine = {
214
+ scan: queryEngine_scan,
215
+ query: queryEngine_query
216
+ };
217
+
218
+ let pluginDefinition = {
219
+ id: "Catalog@1",
220
+ name: "Catalog",
221
+ version: "1",
222
+ extensionPoints: [],
223
+ extensions: [{
224
+ name: "Catalog",
225
+ extensionPointName: name
226
+ }],
227
+ eventCollector: "eventCollector",
228
+ extensionProtocols: [],
229
+ apiSchemaFragment: undefined,
230
+ structure: undefined,
231
+ kind: "Domain"
232
+ };
233
+
234
+ let published_meta = {
235
+ service: name,
236
+ time: "2024-01-01T00:00:00Z",
237
+ msgId: "item-ordered-msg",
238
+ correlationId: "place-order-cmd",
239
+ causationId: "order-placed-event"
240
+ };
241
+
242
+ let published_event = {
243
+ TAG: "ItemOrdered",
244
+ productId: "product-1",
245
+ orderId: "order-1"
246
+ };
247
+
248
+ let published = {
249
+ id: "order-1",
250
+ meta: published_meta,
251
+ event: published_event
252
+ };
253
+
254
+ function commandOf(event$p) {
255
+ let match = DemandExtension.mapIncomingEvent(event$p, pluginDefinition, queryEngine);
256
+ if (match.length !== 1) {
257
+ return Stdlib_JsError.throwWithMessage("expected one slice command");
258
+ }
259
+ let match$1 = match[0];
260
+ if (match$1.TAG === "AbstractPublishAggregateCommand") {
261
+ return match$1._1;
262
+ } else {
263
+ return Stdlib_JsError.throwWithMessage("expected one slice command");
264
+ }
265
+ }
266
+
267
+ async function eventLogOn(name) {
268
+ let Bus = LocalBus$ReventlessLocal.Make({});
269
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(Bus);
270
+ let s = Storage.make(name, [], partitionTag, undefined, opts);
271
+ let storage = await TestRunner$ReventlessLocal.resolve(s.operations);
272
+ let publishJson = async (param, param$1, param$2) => {};
273
+ let Log = DcbEventLog_Operations$ReventlessCore.Make({
274
+ name: name,
275
+ serviceName: name,
276
+ storage: storage,
277
+ publishJson: publishJson
278
+ });
279
+ return {
280
+ read: Log.read,
281
+ append: Log.append,
282
+ readStream: Log.readStream,
283
+ appendStream: Log.appendStream
284
+ };
285
+ }
286
+
287
+ async function runUnderMemory(fn) {
288
+ BackendState$ReventlessLocal.setMemory();
289
+ return await fn();
290
+ }
291
+
292
+ async function runUnderSqlite(fn) {
293
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
294
+ BackendState$ReventlessLocal.setSqlite(db, ":memory:");
295
+ await fn();
296
+ BackendState$ReventlessLocal.setMemory();
297
+ return SqliteDriver$ReventlessLocal.close(db);
298
+ }
299
+
300
+ function deliver(eventLog, cmd) {
301
+ return Effect.runPromise(DemandHandler.handleCommands(undefined, undefined, partitionTag, eventLog, Stream.fromIterable([{
302
+ command: Message$ReventlessCore.decodeCommand$p(CommandTopic_Helpers$ReventlessCore.encodeCommandJson(cmd), Id$Reventless.$$String.schema, commandSchema$1),
303
+ reference: cmd.meta.msgId
304
+ }])));
305
+ }
306
+
307
+ async function recordedDemands(eventLog) {
308
+ return (await eventLog.read([{
309
+ eventTypes: ["DemandRecorded"]
310
+ }], undefined)).events;
311
+ }
312
+
313
+ let counter = {
314
+ contents: 0
315
+ };
316
+
317
+ function fresh(prefix) {
318
+ counter.contents = counter.contents + 1 | 0;
319
+ return prefix + `-` + counter.contents.toString();
320
+ }
321
+
322
+ globalThis.beforeEach(() => DemandHandler.resetCache());
323
+
324
+ let backends = [
325
+ [
326
+ "in-memory",
327
+ runUnderMemory
328
+ ],
329
+ [
330
+ "SQLite",
331
+ runUnderSqlite
332
+ ]
333
+ ];
334
+
335
+ backends.forEach(param => {
336
+ let runUnder = param[1];
337
+ globalThis.describe(`a redelivered fact is recorded once (` + param[0] + ` backend)`, () => {
338
+ globalThis.test("the same published event delivered twice appends one event", async () => await runUnder(async () => {
339
+ let log = await eventLogOn(fresh("RepeatOnce"));
340
+ let first = await deliver(log, commandOf(published));
341
+ let again = await deliver(log, commandOf(published));
342
+ globalThis.expect(first.length).toBe(1);
343
+ globalThis.expect(again).toEqual(first);
344
+ let events = await recordedDemands(log);
345
+ globalThis.expect(events.length).toBe(1);
346
+ let cmd = commandOf(published);
347
+ globalThis.expect(events[0].meta.causationId).toEqual(cmd.meta.msgId);
348
+ globalThis.expect(cmd.meta.causationId).toEqual(published_meta.msgId);
349
+ }));
350
+ globalThis.test("a different order for the same product still counts", async () => await runUnder(async () => {
351
+ let log = await eventLogOn(fresh("RepeatOther"));
352
+ await deliver(log, commandOf(published));
353
+ let newrecord = {...published_meta};
354
+ let second_meta = (newrecord.msgId = "item-ordered-msg-2", newrecord);
355
+ let second_event = {
356
+ TAG: "ItemOrdered",
357
+ productId: "product-1",
358
+ orderId: "order-2"
359
+ };
360
+ let second = {
361
+ id: "order-2",
362
+ meta: second_meta,
363
+ event: second_event
364
+ };
365
+ await deliver(log, commandOf(second));
366
+ await deliver(log, commandOf(published));
367
+ await deliver(log, commandOf(second));
368
+ globalThis.expect((await recordedDemands(log)).length).toBe(2);
369
+ }));
370
+ globalThis.test("a command with a random id is decided every time", async () => await runUnder(async () => {
371
+ let log = await eventLogOn(fresh("RepeatRandom"));
372
+ let cmd = commandOf(published);
373
+ let newrecord = {...cmd};
374
+ newrecord.meta = Message$ReventlessCore.deriveMeta(cmd.meta, undefined);
375
+ await deliver(log, newrecord);
376
+ await deliver(log, newrecord);
377
+ globalThis.expect((await recordedDemands(log)).length).toBe(2);
378
+ }));
379
+ globalThis.test("two concurrent copies append one event", async () => await runUnder(async () => {
380
+ let log = await eventLogOn(fresh("RepeatConcurrent"));
381
+ let cmd = commandOf(published);
382
+ let match = await Promise.all([
383
+ deliver(log, cmd),
384
+ deliver(log, cmd)
385
+ ]);
386
+ let b = match[1];
387
+ let a = match[0];
388
+ globalThis.expect(a.length).toBe(1);
389
+ globalThis.expect(b.length).toBe(1);
390
+ globalThis.expect(Stdlib_Result.isOk(a[0]) && Stdlib_Result.isOk(b[0])).toBe(true);
391
+ globalThis.expect((await recordedDemands(log)).length).toBe(1);
392
+ }));
393
+ });
394
+ });
395
+
396
+ let Message;
397
+
398
+ export {
399
+ opts,
400
+ Message,
401
+ Port,
402
+ DemandSlice,
403
+ DemandDecision,
404
+ DemandHandler,
405
+ partitionTag,
406
+ DemandExtension,
407
+ queryEngine,
408
+ pluginDefinition,
409
+ published,
410
+ commandOf,
411
+ eventLogOn,
412
+ runUnderMemory,
413
+ runUnderSqlite,
414
+ deliver,
415
+ recordedDemands,
416
+ counter,
417
+ fresh,
418
+ backends,
419
+ }
420
+ /* Not a pure module */
@@ -1,6 +1,7 @@
1
- // Verifies Phase 2 of Plan 03: a ReadModel whose `Mapping.sourceName` doesn't
2
- // match any key in `allEventTopics` fails plugin assembly (here: ReadModel
3
- // construction) with a clear error, instead of silently producing zero events.
1
+ // A ReadModel whose `Mapping.sourceName` doesn't match any key in `allEventTopics`
2
+ // fails plugin assembly (here: ReadModel construction) with a clear error, instead
3
+ // of silently producing zero events. A dotted name is an extension point's, and
4
+ // the error then also says how another plugin's facts do reach a read model.
4
5
 
5
6
  open JestGlobals
6
7
  open Reventless
@@ -68,22 +69,77 @@ let allEventTopics: ReventlessInfra.EventTopic.allOutputs = Dict.fromArray([
68
69
  ("ProductCatalogDcbEventLog", {ReventlessInfra.EventTopic.resources: [topicResource]}),
69
70
  ])
70
71
 
72
+ // A Source named like an extension point ("Ordering.Orders"): a read model
73
+ // cannot subscribe to one, so the message must say where to go instead.
74
+ module ExtensionPointSource = {
75
+ module Id = Reventless.Id.String
76
+ let name = "Ordering.Orders"
77
+
78
+ @schema
79
+ type event = ItemOrdered({productId: string, orderId: string})
80
+ }
81
+
82
+ module EpRmSpec = {
83
+ module Id = Reventless.Id.String
84
+ let name = "EpCheckReadModel"
85
+ let moduleUrl: string = %raw(`import.meta.url`)
86
+
87
+ @schema
88
+ type state = {productId: string, orderCount: int}
89
+
90
+ let config = ReadModel.config()
91
+ let subIdConfig = None
92
+ }
93
+
94
+ module EpMapping = Mapping.Make(
95
+ ExtensionPointSource,
96
+ EpRmSpec,
97
+ {
98
+ let project = (msg: Message.event'<ExtensionPointSource.Id.t, ExtensionPointSource.event>) =>
99
+ switch msg.event {
100
+ | ItemOrdered({productId}) =>
101
+ Set(EpRmSpec.Id.makeFromString(productId), ({productId, orderCount: 1}: EpRmSpec.state))
102
+ }
103
+ },
104
+ )
105
+
106
+ module EpMappings: Mappings with module Target := EpRmSpec = {
107
+ module Mappings = Mappings.Make(EpRmSpec)
108
+ module type Mapping = Mappings.Mapping
109
+ let moduleUrl: string = %raw(`import.meta.url`)
110
+ let mappings: array<module(Mapping)> = [module(EpMapping)]
111
+ }
112
+
71
113
  module ReadModelMaker = ReadModel_Builder.Make(Bus)
72
114
  module RmMaker = ReadModelMaker.Make(RmSpec, TypoMappings)
115
+ module EpRmMaker = ReadModelMaker.Make(EpRmSpec, EpMappings)
116
+
117
+ let messageOf = make => {
118
+ let msg = ref(None)
119
+ try {
120
+ let _ = make()
121
+ } catch {
122
+ | JsExn(err) => msg := Some(err->JsExn.message->Option.getOr(""))
123
+ }
124
+ msg.contents
125
+ }
73
126
 
74
127
  describe("ReadModel source-name fail-fast:", () => {
75
128
  testSync("constructing a ReadModel whose Mapping.sourceName is missing throws", () => {
76
- let threw = ref(false)
77
- let msg = ref("")
78
- try {
79
- let _ = RmMaker.make(~api=(), ~apiRole=(), ~allEventTopics)
80
- } catch {
81
- | JsExn(err) =>
82
- threw := true
83
- msg := err->JsExn.message->Option.getOr("")
84
- }
85
- expect(threw.contents)->toBe(true)->ignore
86
- expect(msg.contents->String.includes("MissingDcb"))->toBe(true)->ignore
87
- expect(msg.contents->String.includes("ProductCatalogDcbEventLog"))->toBe(true)
129
+ let msg =
130
+ messageOf(() => RmMaker.make(~api=(), ~apiRole=(), ~allEventTopics))->Option.getOrThrow
131
+ expect(msg->String.includes("MissingDcb"))->toBe(true)
132
+ expect(msg->String.includes("ProductCatalogDcbEventLog"))->toBe(true)
133
+ // A typo keeps the plain message: no extension point is being named.
134
+ expect(msg->String.includes("extension point"))->toBe(false)
135
+ })
136
+
137
+ testSync("a dotted sourceName names the Extension → slice route instead", () => {
138
+ let msg =
139
+ messageOf(() => EpRmMaker.make(~api=(), ~apiRole=(), ~allEventTopics))->Option.getOrThrow
140
+ expect(msg->String.includes("Ordering.Orders"))->toBe(true)
141
+ expect(msg->String.includes("looks like an extension point name"))->toBe(true)
142
+ expect(msg->String.includes("Declare an Extension"))->toBe(true)
143
+ expect(msg->String.includes("Targeting a StateChangeSlice instead of an Aggregate"))->toBe(true)
88
144
  })
89
145
  })
@@ -104,6 +104,84 @@ let allEventTopics = Object.fromEntries([[
104
104
  }
105
105
  ]]);
106
106
 
107
+ let name$2 = "Ordering.Orders";
108
+
109
+ let eventSchema$1 = Sury.$schema(s => ({
110
+ TAG: "ItemOrdered",
111
+ productId: s.m(Sury.string),
112
+ orderId: s.m(Sury.string)
113
+ }));
114
+
115
+ let ExtensionPointSource = {
116
+ Id: undefined,
117
+ name: name$2,
118
+ eventSchema: eventSchema$1
119
+ };
120
+
121
+ let name$3 = "EpCheckReadModel";
122
+
123
+ let moduleUrl$2 = import.meta.url;
124
+
125
+ let stateSchema$1 = Sury.$schema(s => ({
126
+ productId: s.m(Sury.string),
127
+ orderCount: s.m(Sury.int32)
128
+ }));
129
+
130
+ let config$1 = ReadModel$Reventless.config(undefined, undefined, undefined);
131
+
132
+ let EpRmSpec = {
133
+ Id: undefined,
134
+ name: name$3,
135
+ moduleUrl: moduleUrl$2,
136
+ stateSchema: stateSchema$1,
137
+ config: config$1,
138
+ subIdConfig: undefined,
139
+ authorization: "AllowAuthenticated",
140
+ visibility: "Public"
141
+ };
142
+
143
+ function project$1(msg) {
144
+ let match = msg.event;
145
+ let productId = match.productId;
146
+ return {
147
+ TAG: "Set",
148
+ _0: Id$Reventless.$$String.makeFromString(productId),
149
+ _1: {
150
+ productId: productId,
151
+ orderCount: 1
152
+ }
153
+ };
154
+ }
155
+
156
+ let EpMapping = Projection$Reventless.Mapping.Make({
157
+ Id: Id$Reventless.$$String,
158
+ name: name$2,
159
+ eventSchema: eventSchema$1
160
+ })({
161
+ Id: Id$Reventless.$$String,
162
+ name: name$3,
163
+ stateSchema: stateSchema$1,
164
+ subIdConfig: undefined
165
+ })({
166
+ project: project$1
167
+ });
168
+
169
+ Projection$Reventless.Mappings.Make({
170
+ Id: Id$Reventless.$$String,
171
+ name: name$3,
172
+ stateSchema: stateSchema$1,
173
+ subIdConfig: undefined
174
+ });
175
+
176
+ let moduleUrl$3 = import.meta.url;
177
+
178
+ let mappings$1 = [EpMapping];
179
+
180
+ let EpMappings = {
181
+ moduleUrl: moduleUrl$3,
182
+ mappings: mappings$1
183
+ };
184
+
107
185
  let ReadModelMaker = ReadModel_Builder$ReventlessLocal.Make(Bus);
108
186
 
109
187
  let RmMaker = ReadModelMaker.Make({
@@ -117,24 +195,45 @@ let RmMaker = ReadModelMaker.Make({
117
195
  visibility: "Public"
118
196
  })(TypoMappings);
119
197
 
198
+ let EpRmMaker = ReadModelMaker.Make({
199
+ Id: Id$Reventless.$$String,
200
+ name: name$3,
201
+ moduleUrl: moduleUrl$2,
202
+ stateSchema: stateSchema$1,
203
+ config: config$1,
204
+ subIdConfig: undefined,
205
+ authorization: "AllowAuthenticated",
206
+ visibility: "Public"
207
+ })(EpMappings);
208
+
209
+ function messageOf(make) {
210
+ let msg;
211
+ try {
212
+ make();
213
+ } catch (raw_err) {
214
+ let err = Primitive_exceptions.internalToException(raw_err);
215
+ if (err.RE_EXN_ID === "JsExn") {
216
+ msg = Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
217
+ } else {
218
+ throw err;
219
+ }
220
+ }
221
+ return msg;
222
+ }
223
+
120
224
  globalThis.describe("ReadModel source-name fail-fast:", () => {
121
225
  globalThis.test("constructing a ReadModel whose Mapping.sourceName is missing throws", () => {
122
- let threw = false;
123
- let msg = "";
124
- try {
125
- RmMaker.make(undefined, undefined, allEventTopics, undefined, undefined);
126
- } catch (raw_err) {
127
- let err = Primitive_exceptions.internalToException(raw_err);
128
- if (err.RE_EXN_ID === "JsExn") {
129
- threw = true;
130
- msg = Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
131
- } else {
132
- throw err;
133
- }
134
- }
135
- globalThis.expect(threw).toBe(true);
226
+ let msg = Stdlib_Option.getOrThrow(messageOf(() => RmMaker.make(undefined, undefined, allEventTopics, undefined, undefined)), undefined);
136
227
  globalThis.expect(msg.includes("MissingDcb")).toBe(true);
137
228
  globalThis.expect(msg.includes("ProductCatalogDcbEventLog")).toBe(true);
229
+ globalThis.expect(msg.includes("extension point")).toBe(false);
230
+ });
231
+ globalThis.test("a dotted sourceName names the Extension → slice route instead", () => {
232
+ let msg = Stdlib_Option.getOrThrow(messageOf(() => EpRmMaker.make(undefined, undefined, allEventTopics, undefined, undefined)), undefined);
233
+ globalThis.expect(msg.includes("Ordering.Orders")).toBe(true);
234
+ globalThis.expect(msg.includes("looks like an extension point name")).toBe(true);
235
+ globalThis.expect(msg.includes("Declare an Extension")).toBe(true);
236
+ globalThis.expect(msg.includes("Targeting a StateChangeSlice instead of an Aggregate")).toBe(true);
138
237
  });
139
238
  });
140
239
 
@@ -146,7 +245,13 @@ export {
146
245
  TypoMappings,
147
246
  topicResource,
148
247
  allEventTopics,
248
+ ExtensionPointSource,
249
+ EpRmSpec,
250
+ EpMapping,
251
+ EpMappings,
149
252
  ReadModelMaker,
150
253
  RmMaker,
254
+ EpRmMaker,
255
+ messageOf,
151
256
  }
152
257
  /* Bus Not a pure module */
@@ -155,7 +155,7 @@ function encodeEvent(event) {
155
155
  let json = Util_Sury$Reventless.toJson(event, eventSchema);
156
156
  let match = Message$ReventlessCore.splitMessage(json);
157
157
  let tags = DcbTag$Reventless.extractTags(eventSchema, event);
158
- let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined, undefined);
158
+ let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
159
159
  return {
160
160
  eventType: match[0],
161
161
  data: match[1],
@@ -158,7 +158,7 @@ function encodeEvent(event) {
158
158
  let json = Util_Sury$Reventless.toJson(event, eventSchema);
159
159
  let match = Message$ReventlessCore.splitMessage(json);
160
160
  let tags = DcbTag$Reventless.extractTags(eventSchema, event);
161
- let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined, undefined);
161
+ let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
162
162
  return {
163
163
  eventType: match[0],
164
164
  data: match[1],
@@ -153,7 +153,7 @@ function encodeEvent(event) {
153
153
  let json = Util_Sury$Reventless.toJson(event, eventSchema);
154
154
  let match = Message$ReventlessCore.splitMessage(json);
155
155
  let tags = DcbTag$Reventless.extractTags(eventSchema, event);
156
- let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined, undefined);
156
+ let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
157
157
  return {
158
158
  eventType: match[0],
159
159
  data: match[1],