@reventlessdev/reventless-gwt 1.0.0-alpha.207 → 1.0.0-alpha.209

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 (40) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +6 -6
  3. package/src/Automation_GWT.res +7 -17
  4. package/src/Behavior_GWT.res +16 -35
  5. package/src/Delegate_GWT.res +51 -50
  6. package/src/Diff.res +14 -27
  7. package/src/EventMapping_GWT.res +1 -3
  8. package/src/Flow_GWT.res +53 -57
  9. package/src/Hint.res +1 -4
  10. package/src/InboundTranslation_GWT.res +4 -10
  11. package/src/LocalHost.res +13 -9
  12. package/src/Mapping_GWT.res +6 -23
  13. package/src/MismatchRender.res +8 -2
  14. package/src/MultiSourceProjection_GWT.res +6 -15
  15. package/src/OutboundTranslation_GWT.res +12 -28
  16. package/src/Outcome.res +15 -15
  17. package/src/Projection_GWT.res +9 -17
  18. package/src/Query_GWT.res +28 -19
  19. package/src/RenderRescript.res +1 -2
  20. package/src/SideEffect_GWT.res +0 -1
  21. package/src/StateChange/ExternalAddCategorySlice.res +2 -4
  22. package/src/StateChange/WithFixtures.res +3 -6
  23. package/src/StateChange/WithManualOpen.res +3 -6
  24. package/tests/AutomationGwtTest.res +9 -7
  25. package/tests/DelegateGwtTest.res +0 -1
  26. package/tests/FlowAggregateGwtTest.res +2 -5
  27. package/tests/FlowCrossPluginGwtTest.res +65 -40
  28. package/tests/FlowGwtTest.res +0 -1
  29. package/tests/InboundTranslationGwtTest.res +7 -4
  30. package/tests/LocalHostIntegration.res +29 -12
  31. package/tests/LocalHostTest.res +15 -8
  32. package/tests/MappingGwtTest.res +5 -20
  33. package/tests/OutboundTranslationGwtTest.res +7 -4
  34. package/tests/QueryGwtTest.res +10 -12
  35. package/tests/QueryResolverGwtTest.res +4 -4
  36. package/tests/SideEffect/FakeOrderNotification.res +1 -2
  37. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +6 -10
  38. package/tests/StateChange/WithFixtures_GWT.res +0 -1
  39. package/tests/StateChangeSliceGwtTest.res +22 -21
  40. package/tests/StateViewSliceGwtTest.res +5 -17
@@ -7,7 +7,6 @@
7
7
  // command on `o2` does not see `o1`'s prior events.
8
8
  // - The `Error` branch flows through `thenError` like the slice form.
9
9
 
10
-
11
10
  open Flow_GWT
12
11
 
13
12
  // -- Aggregate: CatalogProduct (one event family, no error) -----------------
@@ -16,12 +15,10 @@ module CatalogProductAggregate = {
16
15
  let name = "CatalogProduct"
17
16
 
18
17
  @schema
19
- type command =
20
- | SyncNewProduct({productId: string, name: string})
18
+ type command = SyncNewProduct({productId: string, name: string})
21
19
 
22
20
  @schema
23
- type event =
24
- | CatalogProductSynced({productId: string, name: string})
21
+ type event = CatalogProductSynced({productId: string, name: string})
25
22
 
26
23
  @schema
27
24
  type error = AlreadySynced
@@ -15,7 +15,6 @@
15
15
  // order fans out into one demand command per product across the boundary.
16
16
  // See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phase 3.
17
17
 
18
-
19
18
  open Flow_GWT
20
19
 
21
20
  module EPM = ReventlessInfra.ExtensionPointMapping
@@ -40,7 +39,11 @@ module AddProductSlice = {
40
39
 
41
40
  @schema
42
41
  type event =
43
- ProductAdded({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
42
+ | ProductAdded({
43
+ productId: @s.matches(Reventless.DcbTag.string) string,
44
+ name: string,
45
+ price: float,
46
+ })
44
47
  }
45
48
 
46
49
  module AddProductBehavior = {
@@ -91,7 +94,10 @@ module ProductsEpMapping = {
91
94
  (_id, event: ProductDelegate.event, _meta, _q) =>
92
95
  switch event {
93
96
  | ProductAdded({productId, name, price}) => [
94
- EPM.PublishEvent(productId, ProductsEpSpec.ProductBecameAvailable({productId, name, price})),
97
+ EPM.PublishEvent(
98
+ productId,
99
+ ProductsEpSpec.ProductBecameAvailable({productId, name, price}),
100
+ ),
95
101
  ]
96
102
  },
97
103
  )
@@ -127,9 +133,10 @@ module OrdersEpMapping = {
127
133
  (_id, event: OrderDelegate.event, _meta, _q) =>
128
134
  switch event {
129
135
  | OrderPlaced({orderId, productIds}) =>
130
- productIds->Array.map(pid =>
131
- EPM.PublishEvent(pid, OrdersEpSpec.ItemOrdered({productId: pid, orderId}))
132
- )
136
+ productIds->Array.map(pid => EPM.PublishEvent(
137
+ pid,
138
+ OrdersEpSpec.ItemOrdered({productId: pid, orderId}),
139
+ ))
133
140
  },
134
141
  )
135
142
  }
@@ -164,7 +171,8 @@ module RecordDemandBehavior = {
164
171
  }
165
172
  let decide = (_state, command: RecordDemandSlice.command) =>
166
173
  switch command {
167
- | RecordDemand({productId, orderId}) => Ok([RecordDemandSlice.DemandRecorded({productId, orderId})])
174
+ | RecordDemand({productId, orderId}) =>
175
+ Ok([RecordDemandSlice.DemandRecorded({productId, orderId})])
168
176
  }
169
177
  }
170
178
 
@@ -194,11 +202,19 @@ module SyncProductSlice = {
194
202
  type consumedEvent = ProductSynced({productId: @s.matches(Reventless.DcbTag.string) string})
195
203
  @schema
196
204
  type command =
197
- SyncProduct({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
205
+ | SyncProduct({
206
+ productId: @s.matches(Reventless.DcbTag.string) string,
207
+ name: string,
208
+ price: float,
209
+ })
198
210
  @schema type error = NoError
199
211
  @schema
200
212
  type event =
201
- ProductSynced({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
213
+ | ProductSynced({
214
+ productId: @s.matches(Reventless.DcbTag.string) string,
215
+ name: string,
216
+ price: float,
217
+ })
202
218
  let moduleUrl = ""
203
219
  let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
204
220
  type lifecycleState = unit
@@ -242,20 +258,20 @@ module PlaceOrderSlice = {
242
258
  | ProductSynced({productId: @s.matches(Reventless.DcbTag.string) string})
243
259
  @schema
244
260
  type command =
245
- PlaceOrder({
246
- orderId: @s.matches(Reventless.DcbTag.string) string,
247
- productIds: array<@s.matches(Reventless.DcbTag.stringForKey(~key="productId")) string>,
248
- })
261
+ | PlaceOrder({
262
+ orderId: @s.matches(Reventless.DcbTag.string) string,
263
+ productIds: array<@s.matches(Reventless.DcbTag.stringForKey(~key="productId")) string>,
264
+ })
249
265
  @schema
250
266
  type error =
251
267
  | OrderAlreadyPlaced
252
268
  | ProductsNotAvailable({missing: array<string>})
253
269
  @schema
254
270
  type event =
255
- OrderPlaced({
256
- orderId: @s.matches(Reventless.DcbTag.string) string,
257
- productIds: array<@s.matches(Reventless.DcbTag.stringForKey(~key="productId")) string>,
258
- })
271
+ | OrderPlaced({
272
+ orderId: @s.matches(Reventless.DcbTag.string) string,
273
+ productIds: array<@s.matches(Reventless.DcbTag.stringForKey(~key="productId")) string>,
274
+ })
259
275
  }
260
276
 
261
277
  module PlaceOrderBehavior = {
@@ -265,7 +281,10 @@ module PlaceOrderBehavior = {
265
281
  let evolve = (state, event: PlaceOrderSlice.consumedEvent) =>
266
282
  switch event {
267
283
  | OrderPlaced({orderId}) => {...state, placed: state.placed->Array.concat([orderId])}
268
- | ProductSynced({productId}) => {...state, available: state.available->Array.concat([productId])}
284
+ | ProductSynced({productId}) => {
285
+ ...state,
286
+ available: state.available->Array.concat([productId]),
287
+ }
269
288
  }
270
289
  let decide = (state, command: PlaceOrderSlice.command) =>
271
290
  switch command {
@@ -301,8 +320,12 @@ module Demand = CommandStep(RecordDemandSlice, RecordDemandBehavior)
301
320
  describe("Cross-plugin flow", () => {
302
321
  test("Tier 2 — a product added in Catalog becomes orderable in Ordering via sync", () =>
303
322
  start
304
- ->AddProduct.whenCommand(AddProductSlice.AddProduct({productId: "p1", name: "Book", price: 9.99}))
305
- ->AddProduct.thenEvent(AddProductSlice.ProductAdded({productId: "p1", name: "Book", price: 9.99}))
323
+ ->AddProduct.whenCommand(
324
+ AddProductSlice.AddProduct({productId: "p1", name: "Book", price: 9.99}),
325
+ )
326
+ ->AddProduct.thenEvent(
327
+ AddProductSlice.ProductAdded({productId: "p1", name: "Book", price: 9.99}),
328
+ )
306
329
  ->ProductsEp.whenPublishedThrough
307
330
  ->ProductsEp.thenPublicEvent(
308
331
  ProductsEpSpec.ProductBecameAvailable({productId: "p1", name: "Book", price: 9.99}),
@@ -323,25 +346,27 @@ describe("Cross-plugin flow", () => {
323
346
  ->Place.thenError(ProductsNotAvailable({missing: ["p1"]}))
324
347
  )
325
348
 
326
- test("Tier 3 — a batch order fans out to one demand command per product across the boundary", () =>
327
- start
328
- ->Sync.givenEvents([
329
- SyncProductSlice.ProductSynced({productId: "p1", name: "Book", price: 9.99}),
330
- SyncProductSlice.ProductSynced({productId: "p2", name: "Pen", price: 1.5}),
331
- ])
332
- ->Place.whenCommand(PlaceOrderSlice.PlaceOrder({orderId: "o1", productIds: ["p1", "p2"]}))
333
- ->Place.thenEvent(PlaceOrderSlice.OrderPlaced({orderId: "o1", productIds: ["p1", "p2"]}))
334
- ->OrdersEp.whenPublishedThrough
335
- ->OrdersEp.thenPublicEvents([
336
- OrdersEpSpec.ItemOrdered({productId: "p1", orderId: "o1"}),
337
- OrdersEpSpec.ItemOrdered({productId: "p2", orderId: "o1"}),
338
- ])
339
- ->OrdersExt.whenExtensionReacts
340
- ->OrdersExt.thenIssuesCommands([
341
- RecordDemandSlice.RecordDemand({productId: "p1", orderId: "o1"}),
342
- RecordDemandSlice.RecordDemand({productId: "p2", orderId: "o1"}),
343
- ])
344
- ->Demand.whenCommand(RecordDemandSlice.RecordDemand({productId: "p1", orderId: "o1"}))
345
- ->Demand.thenEvent(RecordDemandSlice.DemandRecorded({productId: "p1", orderId: "o1"}))
349
+ test(
350
+ "Tier 3 — a batch order fans out to one demand command per product across the boundary",
351
+ () =>
352
+ start
353
+ ->Sync.givenEvents([
354
+ SyncProductSlice.ProductSynced({productId: "p1", name: "Book", price: 9.99}),
355
+ SyncProductSlice.ProductSynced({productId: "p2", name: "Pen", price: 1.5}),
356
+ ])
357
+ ->Place.whenCommand(PlaceOrderSlice.PlaceOrder({orderId: "o1", productIds: ["p1", "p2"]}))
358
+ ->Place.thenEvent(PlaceOrderSlice.OrderPlaced({orderId: "o1", productIds: ["p1", "p2"]}))
359
+ ->OrdersEp.whenPublishedThrough
360
+ ->OrdersEp.thenPublicEvents([
361
+ OrdersEpSpec.ItemOrdered({productId: "p1", orderId: "o1"}),
362
+ OrdersEpSpec.ItemOrdered({productId: "p2", orderId: "o1"}),
363
+ ])
364
+ ->OrdersExt.whenExtensionReacts
365
+ ->OrdersExt.thenIssuesCommands([
366
+ RecordDemandSlice.RecordDemand({productId: "p1", orderId: "o1"}),
367
+ RecordDemandSlice.RecordDemand({productId: "p2", orderId: "o1"}),
368
+ ])
369
+ ->Demand.whenCommand(RecordDemandSlice.RecordDemand({productId: "p1", orderId: "o1"}))
370
+ ->Demand.thenEvent(RecordDemandSlice.DemandRecorded({productId: "p1", orderId: "o1"})),
346
371
  )
347
372
  })
@@ -8,7 +8,6 @@
8
8
  // filters the log by its command's tags.
9
9
  // See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phase 2.
10
10
 
11
-
12
11
  open Flow_GWT
13
12
 
14
13
  // -- Slices ------------------------------------------------------------------
@@ -23,12 +23,15 @@ module PaymentWebhookSlice = {
23
23
 
24
24
  describe("PaymentWebhook InboundTranslationSlice", () => {
25
25
  test("completed status emits ConfirmPayment", () =>
26
- whenInput({paymentId: "p1", orderId: "o1", status: "completed"})
27
- ->thenCommand("o1", ConfirmPayment({orderId: "o1", paymentId: "p1"}))
26
+ whenInput({paymentId: "p1", orderId: "o1", status: "completed"})->thenCommand(
27
+ "o1",
28
+ ConfirmPayment({orderId: "o1", paymentId: "p1"}),
29
+ )
28
30
  )
29
31
 
30
32
  test("unknown status surfaces translate error", () =>
31
- whenInput({paymentId: "p1", orderId: "o1", status: "garbage"})
32
- ->thenTranslateError("Unknown payment status: garbage")
33
+ whenInput({paymentId: "p1", orderId: "o1", status: "garbage"})->thenTranslateError(
34
+ "Unknown payment status: garbage",
35
+ )
33
36
  )
34
37
  })
@@ -34,7 +34,10 @@ test("loadGraph cold-loads structures from the real plugins", async () => {
34
34
  deepEqual(g.structures->Array.map(((n, _)) => n), ["Catalog", "Ordering"])
35
35
 
36
36
  let catalog = structureFor(g, "Catalog")->Option.getOrThrow
37
- let category = catalog.aggregates->Array.find((a: Reventless.Plugin.writableDef) => a.name == "Category")->Option.getOrThrow
37
+ let category =
38
+ catalog.aggregates
39
+ ->Array.find((a: Reventless.Plugin.writableDef) => a.name == "Category")
40
+ ->Option.getOrThrow
38
41
  ok(category.producedEventTypes->Array.includes("Catalog.Added"))
39
42
 
40
43
  // An aggregate's declared errors ride the structure beside its events: what the
@@ -45,7 +48,10 @@ test("loadGraph cold-loads structures from the real plugins", async () => {
45
48
  )
46
49
 
47
50
  let ordering = structureFor(g, "Ordering")->Option.getOrThrow
48
- let order = ordering.aggregates->Array.find((a: Reventless.Plugin.writableDef) => a.name == "Order")->Option.getOrThrow
51
+ let order =
52
+ ordering.aggregates
53
+ ->Array.find((a: Reventless.Plugin.writableDef) => a.name == "Order")
54
+ ->Option.getOrThrow
49
55
  ok(order.producedEventTypes->Array.includes("Ordering.Placed"))
50
56
 
51
57
  // extensionPoints (producer side): Catalog owns Catalog.Products, fed by the
@@ -53,20 +59,31 @@ test("loadGraph cold-loads structures from the real plugins", async () => {
53
59
  // subset of the producer's producedEventTypes, the link the event graph draws.
54
60
  let catalogEps = catalog.extensionPoints->Option.getOr([])
55
61
  let productsEp =
56
- catalogEps->Array.find((e: Reventless.Plugin.extensionPointDef) => e.name == "Catalog.Products")->Option.getOrThrow
57
- ok(productsEp.delegateNames->Array.includes("Product"), ~message=productsEp.delegateNames->Array.join(","))
62
+ catalogEps
63
+ ->Array.find((e: Reventless.Plugin.extensionPointDef) => e.name == "Catalog.Products")
64
+ ->Option.getOrThrow
65
+ ok(
66
+ productsEp.delegateNames->Array.includes("Product"),
67
+ ~message=productsEp.delegateNames->Array.join(","),
68
+ )
58
69
  ok(
59
70
  productsEp.sourceEventTypes->Array.includes("Catalog.Added"),
60
71
  ~message=productsEp.sourceEventTypes->Array.join(","),
61
72
  )
62
- let product = catalog.aggregates->Array.find((a: Reventless.Plugin.writableDef) => a.name == "Product")->Option.getOrThrow
73
+ let product =
74
+ catalog.aggregates
75
+ ->Array.find((a: Reventless.Plugin.writableDef) => a.name == "Product")
76
+ ->Option.getOrThrow
63
77
  ok(productsEp.sourceEventTypes->Array.every(e => product.producedEventTypes->Array.includes(e)))
64
78
  })
65
79
 
66
- test("loadGraph is cold — a second call in the same process succeeds (cache-busting works)", async () => {
67
- let plugins = LocalHost.discover(~packageDirs=[catalogDir])
68
- let g1 = await LocalHost.loadGraph(~platformModulePath=platformPath, ~plugins)
69
- let g2 = await LocalHost.loadGraph(~platformModulePath=platformPath, ~plugins)
70
- equal(g1.structures->Array.length, 1)
71
- equal(g2.structures->Array.length, 1)
72
- })
80
+ test(
81
+ "loadGraph is cold — a second call in the same process succeeds (cache-busting works)",
82
+ async () => {
83
+ let plugins = LocalHost.discover(~packageDirs=[catalogDir])
84
+ let g1 = await LocalHost.loadGraph(~platformModulePath=platformPath, ~plugins)
85
+ let g2 = await LocalHost.loadGraph(~platformModulePath=platformPath, ~plugins)
86
+ equal(g1.structures->Array.length, 1)
87
+ equal(g2.structures->Array.length, 1)
88
+ },
89
+ )
@@ -14,9 +14,9 @@ describe("LocalHost.packageNameToPluginName", () => {
14
14
  testPromise("PascalCases scoped / dashed / underscored package names", async () => {
15
15
  expect(LocalHost.packageNameToPluginName("@scope/my-catalog"))->toBe("MyCatalog")
16
16
  expect(LocalHost.packageNameToPluginName("online-shop"))->toBe("OnlineShop")
17
- expect(
18
- LocalHost.packageNameToPluginName("online-shop-aggregates-catalog"),
19
- )->toBe("OnlineShopAggregatesCatalog")
17
+ expect(LocalHost.packageNameToPluginName("online-shop-aggregates-catalog"))->toBe(
18
+ "OnlineShopAggregatesCatalog",
19
+ )
20
20
  })
21
21
  })
22
22
 
@@ -31,10 +31,14 @@ describe("LocalHost name derivation + discovery", () => {
31
31
  let a = NodePath.join([root, "a"])
32
32
  let aSrc = NodePath.join([a, "src"])
33
33
  let _ = await NodeFs.Promises.mkdir(aSrc, {recursive: true})
34
- let _ =
35
- await NodeFs.Promises.writeFile(NodePath.join([a, "package.json"]), `{"name":"@x/a-pkg"}`)
36
- let _ =
37
- await NodeFs.Promises.writeFile(NodePath.join([aSrc, "plugin.json"]), `{"name":"Catalog"}`)
34
+ let _ = await NodeFs.Promises.writeFile(
35
+ NodePath.join([a, "package.json"]),
36
+ `{"name":"@x/a-pkg"}`,
37
+ )
38
+ let _ = await NodeFs.Promises.writeFile(
39
+ NodePath.join([aSrc, "plugin.json"]),
40
+ `{"name":"Catalog"}`,
41
+ )
38
42
  let _ = await NodeFs.Promises.writeFile(NodePath.join([aSrc, "Plugin.res.mjs"]), "")
39
43
 
40
44
  // b: no plugin.json → name falls back to PascalCase(package.json name).
@@ -55,7 +59,10 @@ describe("LocalHost name derivation + discovery", () => {
55
59
  expect(LocalHost.derivePluginName(~pluginSrcDir=bSrc))->toBe("MyOrdering")
56
60
 
57
61
  let refs = LocalHost.discover(~packageDirs=[a, b, c])
58
- expect(refs->Array.map((r: LocalHost.pluginRef) => r.name))->toEqual(["Catalog", "MyOrdering"])
62
+ expect(refs->Array.map((r: LocalHost.pluginRef) => r.name))->toEqual([
63
+ "Catalog",
64
+ "MyOrdering",
65
+ ])
59
66
  let first: LocalHost.pluginRef = refs->Array.getUnsafe(0)
60
67
  expect(first.modulePath->String.endsWith("Plugin.res.mjs"))->toBe(true)
61
68
  expect(first.packageDir)->toBe(a)
@@ -6,7 +6,6 @@
6
6
  // target-decide pipeline end-to-end against the unified `GwtSource`/`GwtTarget`
7
7
  // module types.
8
8
 
9
-
10
9
  // ---------------------------------------------------------------------------
11
10
  // Shared Aggregate specs + behaviours used by the Aggr→* cases below.
12
11
  // ---------------------------------------------------------------------------
@@ -98,9 +97,7 @@ module NotificationSlice = {
98
97
 
99
98
  @schema
100
99
  type consumedEvent =
101
- | NotificationSent({
102
- notificationId: @s.matches(Reventless.DcbTag.string) string,
103
- })
100
+ NotificationSent({notificationId: @s.matches(Reventless.DcbTag.string) string})
104
101
 
105
102
  let evolve = (_state, event) =>
106
103
  switch event {
@@ -140,8 +137,7 @@ module InventorySlice = {
140
137
  let initialState = {reserved: false}
141
138
 
142
139
  @schema
143
- type consumedEvent =
144
- | StockReserved({itemId: @s.matches(Reventless.DcbTag.string) string})
140
+ type consumedEvent = StockReserved({itemId: @s.matches(Reventless.DcbTag.string) string})
145
141
 
146
142
  let evolve = (_state, event) =>
147
143
  switch event {
@@ -149,21 +145,13 @@ module InventorySlice = {
149
145
  }
150
146
 
151
147
  @schema
152
- type command =
153
- | ReserveStock({
154
- itemId: @s.matches(Reventless.DcbTag.string) string,
155
- quantity: int,
156
- })
148
+ type command = ReserveStock({itemId: @s.matches(Reventless.DcbTag.string) string, quantity: int})
157
149
 
158
150
  @schema
159
151
  type error = OutOfStock
160
152
 
161
153
  @schema
162
- type event =
163
- | StockReserved({
164
- itemId: @s.matches(Reventless.DcbTag.string) string,
165
- quantity: int,
166
- })
154
+ type event = StockReserved({itemId: @s.matches(Reventless.DcbTag.string) string, quantity: int})
167
155
 
168
156
  let decide = (state, command) =>
169
157
  switch command {
@@ -337,10 +325,7 @@ DcbToDcbGwt.describe("Inventory → Notification (DCB → DCB)", () => {
337
325
  DcbToDcbGwt.test("existing NotificationSent causes target decide to reject", () =>
338
326
  DcbToDcbGwt.givenSourceEvents([])
339
327
  ->DcbToDcbGwt.andTargetEvents([
340
- (
341
- "reserved-i7",
342
- [NotificationSlice.NotificationSent({notificationId: "reserved-i7"})],
343
- ),
328
+ ("reserved-i7", [NotificationSlice.NotificationSent({notificationId: "reserved-i7"})]),
344
329
  ])
345
330
  ->DcbToDcbGwt.whenSourceCmd("i7", ReserveStock({itemId: "i7", quantity: 2}))
346
331
  ->DcbToDcbGwt.thenTargetError(NotificationAlreadySent)
@@ -44,10 +44,13 @@ describe("SendTrackingEmail OutboundTranslationSlice", () => {
44
44
  test("retrying translate succeeds on the third attempt → 2 retries recorded", () => {
45
45
  let calls = ref(0)
46
46
  givenTodo("o1", {orderId: "o1", email: "x@y"})
47
- ->whenTranslateRetrying(~maxRetries=3, (_id, _item) => {
48
- calls := calls.contents + 1
49
- Promise.resolve(calls.contents < 3 ? Error("smtp down") : Ok(None))
50
- })
47
+ ->whenTranslateRetrying(
48
+ ~maxRetries=3,
49
+ (_id, _item) => {
50
+ calls := calls.contents + 1
51
+ Promise.resolve(calls.contents < 3 ? Error("smtp down") : Ok(None))
52
+ },
53
+ )
51
54
  ->thenRetryRecorded(2)
52
55
  })
53
56
 
@@ -2,7 +2,6 @@
2
2
  // to prove the unified DSL works for either consumer type. Stage 6 of
3
3
  // `docs/plans/done/reventless-gwt.md`.
4
4
 
5
-
6
5
  // ---------------------------------------------------------------------------
7
6
  // Case 1 — ReadModel: Categories.
8
7
  // ---------------------------------------------------------------------------
@@ -51,9 +50,7 @@ CategoriesQuery.describe("Categories ReadModel queries", () => {
51
50
 
52
51
  CategoriesQuery.test("missing index produces QueryRowsMismatch", () => {
53
52
  let outcome =
54
- CategoriesQuery.givenStore([
55
- ("c1", {categoryId: "c1", name: "Electronics", archived: false}),
56
- ])
53
+ CategoriesQuery.givenStore([("c1", {categoryId: "c1", name: "Electronics", archived: false})])
57
54
  ->CategoriesQuery.whenQuery({by: "name", value: "Electronics", index: "byArchived"})
58
55
  ->CategoriesQuery.thenRows([])
59
56
  switch outcome {
@@ -82,11 +79,14 @@ CategoriesQuery.describe("Categories ReadModel queries", () => {
82
79
 
83
80
  CategoriesQuery.test("limit truncates results and thenRowCount verifies", () =>
84
81
  CategoriesQuery.givenStore(
85
- Array.fromInitializer(~length=10, i => {
86
- let id = "c" ++ i->Int.toString
87
- let state: CategoriesReadModel.state = {categoryId: id, name: "Books", archived: false}
88
- (id, state)
89
- }),
82
+ Array.fromInitializer(
83
+ ~length=10,
84
+ i => {
85
+ let id = "c" ++ i->Int.toString
86
+ let state: CategoriesReadModel.state = {categoryId: id, name: "Books", archived: false}
87
+ (id, state)
88
+ },
89
+ ),
90
90
  )
91
91
  ->CategoriesQuery.whenQuery({by: "name", value: "Books", index: "byName", limit: 3})
92
92
  ->CategoriesQuery.thenRowCount(3)
@@ -145,9 +145,7 @@ OrdersQuery.describe("OrdersView StateViewSlice queries", () => {
145
145
  ("ord-2", "cust-b", {orderId: "ord-2", customerId: "cust-b", total: 200}),
146
146
  ])
147
147
  ->OrdersQuery.whenQueryByCompositeId({id: "ord-1", subId: "cust-a"})
148
- ->OrdersQuery.thenRowFromComposite(
149
- Some({orderId: "ord-1", customerId: "cust-a", total: 100}),
150
- )
148
+ ->OrdersQuery.thenRowFromComposite(Some({orderId: "ord-1", customerId: "cust-a", total: 100}))
151
149
  )
152
150
 
153
151
  OrdersQuery.test("composite-id lookup with wrong subId returns None", () =>
@@ -3,7 +3,6 @@
3
3
  // productId (and a productIds array) that resolve into a Products read model;
4
4
  // the resolver DSL follows those keys across the spec boundary.
5
5
 
6
-
7
6
  module RM = Reventless.ReadModel
8
7
 
9
8
  module ProductRow = {
@@ -61,9 +60,10 @@ R.describe("Cross-spec resolvers (MakeResolver)", () => {
61
60
  )
62
61
 
63
62
  R.test("resolving an unknown foreign key returns no row", () =>
64
- R.givenStores([("order2", {OrderRow.orderId: "order2", productId: "px", productIds: []})], [
65
- ("p1", book),
66
- ])
63
+ R.givenStores(
64
+ [("order2", {OrderRow.orderId: "order2", productId: "px", productIds: []})],
65
+ [("p1", book)],
66
+ )
67
67
  ->R.whenResolve(~field="productId", "order2")
68
68
  ->R.thenResolved(None)
69
69
  )
@@ -17,7 +17,6 @@ let moduleUrl = "test://FakeOrderNotification"
17
17
 
18
18
  let execute = async (id, _meta, event, _queryEngine) =>
19
19
  switch event {
20
- | Source.Placed({email}) =>
21
- MockEmail.recordSend(~email, ~orderId=Source.Id.toString(id))
20
+ | Source.Placed({email}) => MockEmail.recordSend(~email, ~orderId=Source.Id.toString(id))
22
21
  | Source.Shipped => ()
23
22
  }
@@ -8,14 +8,9 @@ describe("FakeOrderNotification SideEffect", () => {
8
8
  JestGlobals.beforeEach(MockEmail.reset)
9
9
 
10
10
  test("Placed records a confirmation send", () =>
11
- givenEventForId(
12
- Source.Id.makeFromString("o1"),
13
- Source.Placed({email: "alice@example.com"}),
14
- )
11
+ givenEventForId(Source.Id.makeFromString("o1"), Source.Placed({email: "alice@example.com"}))
15
12
  ->whenExecuted(MockEmail.mock)
16
- ->thenExternalCalls([
17
- MockEmail.SendConfirmation({email: "alice@example.com", orderId: "o1"}),
18
- ])
13
+ ->thenExternalCalls([MockEmail.SendConfirmation({email: "alice@example.com", orderId: "o1"})])
19
14
  )
20
15
 
21
16
  test("Shipped is a no-op", () =>
@@ -25,9 +20,10 @@ describe("FakeOrderNotification SideEffect", () => {
25
20
  )
26
21
 
27
22
  test("Two distinct Placed events record two calls", async () => {
28
- let _ =
29
- await givenEventForId(Source.Id.makeFromString("o1"), Source.Placed({email: "a@a"}))
30
- ->whenExecuted(MockEmail.mock)
23
+ let _ = await givenEventForId(
24
+ Source.Id.makeFromString("o1"),
25
+ Source.Placed({email: "a@a"}),
26
+ )->whenExecuted(MockEmail.mock)
31
27
  await givenEventForId(Source.Id.makeFromString("o2"), Source.Placed({email: "b@b"}))
32
28
  ->whenExecuted(MockEmail.mock)
33
29
  ->thenExternalCallCount(2)
@@ -20,4 +20,3 @@ describe("WithFixtures StateChangeSlice (companion fixtures auto-open)", () => {
20
20
  ->thenEvent((electronicsCategoryAdded :> event))
21
21
  )
22
22
  })
23
-
@@ -118,10 +118,7 @@ module MissingTagBehavior = {
118
118
 
119
119
  let evolve = (state: state, _event: Spec.consumedEvent): state => state
120
120
 
121
- let decide = (
122
- _state: state,
123
- cmd: Spec.command,
124
- ): result<array<Spec.event>, Spec.error> =>
121
+ let decide = (_state: state, cmd: Spec.command): result<array<Spec.event>, Spec.error> =>
125
122
  switch cmd {
126
123
  | Do({id}) => Ok([Spec.Done({id: id})])
127
124
  }
@@ -130,23 +127,27 @@ module MissingTagBehavior = {
130
127
  module MissingTagGwt = Behavior_GWT.Make(MissingTagSlice, MissingTagBehavior)
131
128
 
132
129
  MissingTagGwt.describe("MissingTag slice implicit check", () => {
133
- MissingTagGwt.test("thenEvent surfaces AppendConditionMismatch when command lacks DCB tag", () => {
134
- let outcome =
135
- MissingTagGwt.givenEvents([])
136
- ->MissingTagGwt.whenCmd(Do({id: "x1"}))
137
- ->MissingTagGwt.thenEvent(Done({id: "x1"}))
138
- switch outcome {
139
- | Error(AppendConditionMismatch(_)) => Outcome.pass
140
- | Error(other) =>
141
- Outcome.fail(
142
- Throw({error: "expected AppendConditionMismatch, got: " ++ Outcome.kindName(other), stack: ""}),
143
- )
144
- | Ok() =>
145
- Outcome.fail(
146
- Throw({error: "expected AppendConditionMismatch, got pass", stack: ""}),
147
- )
148
- }
149
- })
130
+ MissingTagGwt.test(
131
+ "thenEvent surfaces AppendConditionMismatch when command lacks DCB tag",
132
+ () => {
133
+ let outcome =
134
+ MissingTagGwt.givenEvents([])
135
+ ->MissingTagGwt.whenCmd(Do({id: "x1"}))
136
+ ->MissingTagGwt.thenEvent(Done({id: "x1"}))
137
+ switch outcome {
138
+ | Error(AppendConditionMismatch(_)) => Outcome.pass
139
+ | Error(other) =>
140
+ Outcome.fail(
141
+ Throw({
142
+ error: "expected AppendConditionMismatch, got: " ++ Outcome.kindName(other),
143
+ stack: "",
144
+ }),
145
+ )
146
+ | Ok() =>
147
+ Outcome.fail(Throw({error: "expected AppendConditionMismatch, got pass", stack: ""}))
148
+ }
149
+ },
150
+ )
150
151
 
151
152
  MissingTagGwt.test(
152
153
  "thenAppendsConditionedOnExactly bypasses implicit check (still passes with derived)",
@@ -31,15 +31,9 @@ module CategoriesViewProjection = {
31
31
 
32
32
  let project = ({event}: Reventless.StateViewSlice.consumed<consumedEvent>) =>
33
33
  switch event {
34
- | CategoryAdded({categoryId, name}) => [
35
- Set(categoryId, {categoryId, name, archived: false}),
36
- ]
37
- | CategoryRenamed({categoryId, name}) => [
38
- Update(categoryId, state => {...state, name}),
39
- ]
40
- | CategoryArchived({categoryId}) => [
41
- Update(categoryId, state => {...state, archived: true}),
42
- ]
34
+ | CategoryAdded({categoryId, name}) => [Set(categoryId, {categoryId, name, archived: false})]
35
+ | CategoryRenamed({categoryId, name}) => [Update(categoryId, state => {...state, name})]
36
+ | CategoryArchived({categoryId}) => [Update(categoryId, state => {...state, archived: true})]
43
37
  }
44
38
  }
45
39
 
@@ -47,18 +41,12 @@ describe("CategoriesView StateViewSlice", () => {
47
41
  test("projects CategoryAdded into a new row", () =>
48
42
  givenEvents([])
49
43
  ->whenEvent(CategoryAdded({categoryId: "c1", name: "Electronics"}))
50
- ->thenStateWithId(
51
- "c1",
52
- {categoryId: "c1", name: "Electronics", archived: false},
53
- )
44
+ ->thenStateWithId("c1", {categoryId: "c1", name: "Electronics", archived: false})
54
45
  )
55
46
 
56
47
  test("CategoryRenamed updates the name", () =>
57
48
  givenEvents([CategoryAdded({categoryId: "c1", name: "Electronics"})])
58
49
  ->whenEvent(CategoryRenamed({categoryId: "c1", name: "Consumer Electronics"}))
59
- ->thenStateWithId(
60
- "c1",
61
- {categoryId: "c1", name: "Consumer Electronics", archived: false},
62
- )
50
+ ->thenStateWithId("c1", {categoryId: "c1", name: "Consumer Electronics", archived: false})
63
51
  )
64
52
  })