@reventlessdev/reventless-gwt 1.0.0-alpha.74

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 (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +603 -0
  10. package/src/Behavior_GWT.res.mjs +426 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +137 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +31 -0
  16. package/src/Cancellation.res.mjs +45 -0
  17. package/src/ChildProcess.res +84 -0
  18. package/src/ChildProcess.res.mjs +92 -0
  19. package/src/Cli.res +671 -0
  20. package/src/Cli.res.mjs +754 -0
  21. package/src/Collector.res +201 -0
  22. package/src/Collector.res.mjs +215 -0
  23. package/src/ComponentMeta.res +104 -0
  24. package/src/ComponentMeta.res.mjs +120 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +103 -0
  33. package/src/DomainDeadCode.res +93 -0
  34. package/src/DomainDeadCode.res.mjs +69 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +36 -0
  40. package/src/Filter.res.mjs +40 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +138 -0
  44. package/src/FormatterHuman.res.mjs +122 -0
  45. package/src/FormatterJson.res +257 -0
  46. package/src/FormatterJson.res.mjs +320 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +139 -0
  50. package/src/FormatterTap.res.mjs +149 -0
  51. package/src/FormatterVsCode.res +362 -0
  52. package/src/FormatterVsCode.res.mjs +564 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +135 -0
  62. package/src/LocalHost.res.mjs +127 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MultiSourceProjection_GWT.res +316 -0
  66. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  67. package/src/OutboundTranslation_GWT.res +238 -0
  68. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  69. package/src/Outcome.res +161 -0
  70. package/src/Outcome.res.mjs +140 -0
  71. package/src/PackageScan.res +80 -0
  72. package/src/PackageScan.res.mjs +81 -0
  73. package/src/PlatformRunner.res +160 -0
  74. package/src/PlatformRunner.res.mjs +155 -0
  75. package/src/PlatformScan.res +138 -0
  76. package/src/PlatformScan.res.mjs +131 -0
  77. package/src/ProcessManager.res +91 -0
  78. package/src/ProcessManager.res.mjs +80 -0
  79. package/src/Projection_GWT.res +312 -0
  80. package/src/Projection_GWT.res.mjs +332 -0
  81. package/src/Query_GWT.res +379 -0
  82. package/src/Query_GWT.res.mjs +367 -0
  83. package/src/RenderRescript.res +125 -0
  84. package/src/RenderRescript.res.mjs +141 -0
  85. package/src/RunnerTypes.res +68 -0
  86. package/src/RunnerTypes.res.mjs +60 -0
  87. package/src/SideEffect_GWT.res +157 -0
  88. package/src/SideEffect_GWT.res.mjs +113 -0
  89. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  90. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  91. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  92. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  93. package/src/StateChange/WithFixtures.res +22 -0
  94. package/src/StateChange/WithFixtures.res.mjs +31 -0
  95. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  96. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  97. package/src/StateChange/WithManualOpen.res +23 -0
  98. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  99. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  100. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  101. package/src/StubRuntime.res +53 -0
  102. package/src/StubRuntime.res.mjs +54 -0
  103. package/src/TestFixtures.res +16 -0
  104. package/src/TestFixtures.res.mjs +34 -0
  105. package/src/Watch.res +92 -0
  106. package/src/Watch.res.mjs +83 -0
  107. package/src/WatcherProbe.res +48 -0
  108. package/src/WatcherProbe.res.mjs +46 -0
  109. package/tests/AutomationGwtTest.res +71 -0
  110. package/tests/AutomationGwtTest.res.mjs +189 -0
  111. package/tests/BuildClassifierTest.res +94 -0
  112. package/tests/BuildClassifierTest.res.mjs +132 -0
  113. package/tests/ComponentMetaTest.res +52 -0
  114. package/tests/ComponentMetaTest.res.mjs +57 -0
  115. package/tests/DelegateGwtTest.res +487 -0
  116. package/tests/DelegateGwtTest.res.mjs +891 -0
  117. package/tests/DiscoveryTest.res +42 -0
  118. package/tests/DiscoveryTest.res.mjs +43 -0
  119. package/tests/DomainDeadCodeTest.res +116 -0
  120. package/tests/DomainDeadCodeTest.res.mjs +155 -0
  121. package/tests/DomainGraphTest.res +365 -0
  122. package/tests/DomainGraphTest.res.mjs +413 -0
  123. package/tests/FlowAggregateGwtTest.res +169 -0
  124. package/tests/FlowAggregateGwtTest.res.mjs +269 -0
  125. package/tests/FlowCrossPluginGwtTest.res +340 -0
  126. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  127. package/tests/FlowGwtTest.res +222 -0
  128. package/tests/FlowGwtTest.res.mjs +432 -0
  129. package/tests/FormatterVsCodeMessageTest.res +41 -0
  130. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  131. package/tests/InboundTranslationGwtTest.res +34 -0
  132. package/tests/InboundTranslationGwtTest.res.mjs +98 -0
  133. package/tests/LocalHostIntegration.res +70 -0
  134. package/tests/LocalHostIntegration.res.mjs +64 -0
  135. package/tests/LocalHostTest.res +71 -0
  136. package/tests/LocalHostTest.res.mjs +63 -0
  137. package/tests/MappingGwtTest.res +348 -0
  138. package/tests/MappingGwtTest.res.mjs +592 -0
  139. package/tests/OutboundTranslationGwtTest.res +59 -0
  140. package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
  141. package/tests/PlatformRunnerTest.res +66 -0
  142. package/tests/PlatformRunnerTest.res.mjs +85 -0
  143. package/tests/PlatformScanTest.res +61 -0
  144. package/tests/PlatformScanTest.res.mjs +56 -0
  145. package/tests/QueryGwtTest.res +161 -0
  146. package/tests/QueryGwtTest.res.mjs +314 -0
  147. package/tests/QueryResolverGwtTest.res +71 -0
  148. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  149. package/tests/RunnerUnitTest.res +219 -0
  150. package/tests/RunnerUnitTest.res.mjs +244 -0
  151. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  152. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  153. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  154. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  155. package/tests/SideEffect/MockEmail.res +18 -0
  156. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  157. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  158. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  159. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  160. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  161. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  162. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  163. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  164. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  165. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  166. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  167. package/tests/StateChangeSliceGwtTest.res +160 -0
  168. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  169. package/tests/StateViewSliceGwtTest.res +64 -0
  170. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  171. package/tests/WatchDebounceTest.res +78 -0
  172. package/tests/WatchDebounceTest.res.mjs +97 -0
@@ -0,0 +1,413 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
+ import * as DomainGraph$ReventlessGwt from "../src/DomainGraph.res.mjs";
6
+
7
+ function command(name, mutationField, apiExposedOpt) {
8
+ let apiExposed = apiExposedOpt !== undefined ? Primitive_option.valFromOption(apiExposedOpt) : undefined;
9
+ return {
10
+ name: name,
11
+ schema: "",
12
+ level: "Instance",
13
+ aggregateIdField: undefined,
14
+ mutationField: mutationField,
15
+ references: [],
16
+ allowedStates: undefined,
17
+ apiExposed: apiExposed
18
+ };
19
+ }
20
+
21
+ function evt(name) {
22
+ return {
23
+ name: name,
24
+ schema: "",
25
+ references: []
26
+ };
27
+ }
28
+
29
+ function writable(name, commandsOpt, producesOpt, consumesOpt, linkedViewsOpt, eventsOpt) {
30
+ let commands = commandsOpt !== undefined ? commandsOpt : [];
31
+ let produces = producesOpt !== undefined ? producesOpt : [];
32
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
33
+ let linkedViews = linkedViewsOpt !== undefined ? linkedViewsOpt : [];
34
+ let events = eventsOpt !== undefined ? eventsOpt : [];
35
+ return {
36
+ name: name,
37
+ commands: commands,
38
+ producedEventTypes: produces,
39
+ consumedEventTypes: consumes,
40
+ linkedViews: linkedViews,
41
+ consistencyRead: undefined,
42
+ events: events
43
+ };
44
+ }
45
+
46
+ function queryable(name, consumesOpt, visibilityOpt) {
47
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
48
+ let visibility = visibilityOpt !== undefined ? Primitive_option.valFromOption(visibilityOpt) : undefined;
49
+ return {
50
+ name: name,
51
+ queryField: "",
52
+ schema: "",
53
+ consumedEventTypes: consumes,
54
+ linkedWriteSide: [],
55
+ labelField: "id",
56
+ searchableFields: [],
57
+ statusField: undefined,
58
+ visibility: visibility
59
+ };
60
+ }
61
+
62
+ function automation(name, consumesOpt, producesOpt, targetOpt) {
63
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
64
+ let produces = producesOpt !== undefined ? producesOpt : [];
65
+ let target = targetOpt !== undefined ? targetOpt : "";
66
+ return {
67
+ name: name,
68
+ consumedEventTypes: consumes,
69
+ producedCommandTypes: produces,
70
+ targetName: target
71
+ };
72
+ }
73
+
74
+ function inbound(name, producesOpt, targetOpt) {
75
+ let produces = producesOpt !== undefined ? producesOpt : [];
76
+ let target = targetOpt !== undefined ? targetOpt : "";
77
+ return {
78
+ name: name,
79
+ commandTypes: produces,
80
+ targetName: target
81
+ };
82
+ }
83
+
84
+ function outbound(name, consumesOpt, producesOpt, targetOpt) {
85
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
86
+ let produces = producesOpt !== undefined ? producesOpt : [];
87
+ let target = targetOpt !== undefined ? Primitive_option.valFromOption(targetOpt) : undefined;
88
+ return {
89
+ name: name,
90
+ consumedEventTypes: consumes,
91
+ inboundCommandTypes: produces,
92
+ targetName: target
93
+ };
94
+ }
95
+
96
+ function structure(aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionPointsOpt) {
97
+ let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
98
+ let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
99
+ let stateViewSlices = stateViewSlicesOpt !== undefined ? stateViewSlicesOpt : [];
100
+ let stateChangeSlices = stateChangeSlicesOpt !== undefined ? stateChangeSlicesOpt : [];
101
+ let automationSlices = automationSlicesOpt !== undefined ? automationSlicesOpt : [];
102
+ let outboundTranslationSlices = outboundTranslationSlicesOpt !== undefined ? outboundTranslationSlicesOpt : [];
103
+ let inboundTranslationSlices = inboundTranslationSlicesOpt !== undefined ? inboundTranslationSlicesOpt : [];
104
+ let extensionPoints = extensionPointsOpt !== undefined ? extensionPointsOpt : [];
105
+ return {
106
+ readModels: readModels,
107
+ stateViewSlices: stateViewSlices,
108
+ stateChangeSlices: stateChangeSlices,
109
+ aggregates: aggregates,
110
+ automationSlices: automationSlices,
111
+ outboundTranslationSlices: outboundTranslationSlices,
112
+ inboundTranslationSlices: inboundTranslationSlices,
113
+ extensions: [],
114
+ extensionPoints: extensionPoints
115
+ };
116
+ }
117
+
118
+ function hasEdge(g, from, to, kind) {
119
+ return g.edges.some(e => {
120
+ if (e.from === from && e.to === to) {
121
+ return e.kind === kind;
122
+ } else {
123
+ return false;
124
+ }
125
+ });
126
+ }
127
+
128
+ function nodeKind(g, id) {
129
+ return Stdlib_Option.map(g.nodes.find(n => n.id === id), n => n.kind);
130
+ }
131
+
132
+ globalThis.describe("DomainGraph.build", () => {
133
+ globalThis.test("Command → Aggregate → Event → ReadModel (DCB consumedEventTypes)", async () => {
134
+ let shop = structure([writable("Order", [command("Place", "Shop_Order_Place", undefined)], ["Shop.Placed"], undefined, undefined, undefined)], undefined, [queryable("OrderView", ["Shop.Placed"], undefined)], undefined, undefined, undefined, undefined, undefined);
135
+ let g = DomainGraph$ReventlessGwt.build([[
136
+ "Shop",
137
+ shop
138
+ ]]);
139
+ globalThis.expect(nodeKind(g, "Shop_Order_Place")).toEqual("Command");
140
+ globalThis.expect(nodeKind(g, "Shop:Order")).toEqual("Aggregate");
141
+ globalThis.expect(nodeKind(g, "Shop.Placed")).toEqual("Event");
142
+ globalThis.expect(nodeKind(g, "Shop:OrderView")).toEqual("StateViewSlice");
143
+ globalThis.expect(hasEdge(g, "Shop_Order_Place", "Shop:Order", "handles")).toBe(true);
144
+ globalThis.expect(hasEdge(g, "Shop:Order", "Shop.Placed", "emits")).toBe(true);
145
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:OrderView", "projects")).toBe(true);
146
+ });
147
+ globalThis.test("payload-less events (in `events`, not producedEventTypes) still get an emitted node", async () => {
148
+ let shop = structure([writable("Category", undefined, ["Catalog.Added"], undefined, undefined, [
149
+ {
150
+ name: "Added",
151
+ schema: "",
152
+ references: []
153
+ },
154
+ {
155
+ name: "Archived",
156
+ schema: "",
157
+ references: []
158
+ }
159
+ ])], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
160
+ let g = DomainGraph$ReventlessGwt.build([[
161
+ "Catalog",
162
+ shop
163
+ ]]);
164
+ globalThis.expect(nodeKind(g, "Catalog.Added")).toEqual("Event");
165
+ globalThis.expect(nodeKind(g, "Catalog.Archived")).toEqual("Event");
166
+ globalThis.expect(hasEdge(g, "Catalog:Category", "Catalog.Added", "emits")).toBe(true);
167
+ globalThis.expect(hasEdge(g, "Catalog:Category", "Catalog.Archived", "emits")).toBe(true);
168
+ });
169
+ globalThis.test("an Internal StateViewSlice still gets a node + projects edge (dev graph shows it)", async () => {
170
+ let shop = structure([writable("Order", undefined, ["Shop.Placed"], undefined, undefined, undefined)], undefined, [queryable("AvailableView", ["Shop.Placed"], "Internal")], undefined, undefined, undefined, undefined, undefined);
171
+ let g = DomainGraph$ReventlessGwt.build([[
172
+ "Shop",
173
+ shop
174
+ ]]);
175
+ globalThis.expect(nodeKind(g, "Shop:AvailableView")).toEqual("StateViewSlice");
176
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:AvailableView", "projects")).toBe(true);
177
+ });
178
+ globalThis.test("classic linkedViews draws each produced event into the linked read model", async () => {
179
+ let shop = structure([writable("Order", undefined, [
180
+ "Shop.Placed",
181
+ "Shop.Shipped"
182
+ ], undefined, ["Orders"], undefined)], [queryable("Orders", undefined, undefined)], undefined, undefined, undefined, undefined, undefined, undefined);
183
+ let g = DomainGraph$ReventlessGwt.build([[
184
+ "Shop",
185
+ shop
186
+ ]]);
187
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:Orders", "projects")).toBe(true);
188
+ globalThis.expect(hasEdge(g, "Shop.Shipped", "Shop:Orders", "projects")).toBe(true);
189
+ });
190
+ globalThis.test("automation slices are triggered by their consumed events", async () => {
191
+ let shop = structure([writable("Order", undefined, ["Shop.Placed"], undefined, undefined, undefined)], undefined, undefined, undefined, [automation("Notify", ["Shop.Placed"], undefined, undefined)], undefined, undefined, undefined);
192
+ let g = DomainGraph$ReventlessGwt.build([[
193
+ "Shop",
194
+ shop
195
+ ]]);
196
+ globalThis.expect(nodeKind(g, "Shop:Notify")).toEqual("AutomationSlice");
197
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:Notify", "triggers")).toBe(true);
198
+ });
199
+ globalThis.test("an automation routes to the specific command it raises on its target", async () => {
200
+ let shop = structure(undefined, undefined, undefined, [writable("Ship", [
201
+ command("ShipOrder", "Shop_Ship_ShipOrder", undefined),
202
+ command("Place", "Shop_Ship_Place", undefined)
203
+ ], ["Shop.Shipped"], undefined, undefined, undefined)], [automation("AutoShip", ["Shop.Placed"], ["AutoShip.ShipOrder"], "Ship")], undefined, undefined, undefined);
204
+ let g = DomainGraph$ReventlessGwt.build([[
205
+ "Shop",
206
+ shop
207
+ ]]);
208
+ globalThis.expect(hasEdge(g, "Shop:AutoShip", "Shop_Ship_ShipOrder", "delegatesTo")).toBe(true);
209
+ globalThis.expect(hasEdge(g, "Shop:AutoShip", "Shop_Ship_Place", "delegatesTo")).toBe(false);
210
+ });
211
+ globalThis.test("an inbound translation routes to the command it raises on its target", async () => {
212
+ let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Add", "Shop_Product_Add", undefined)], undefined, undefined, undefined, undefined)], undefined, undefined, [inbound("ImportProduct", ["ImportProduct.Add"], "Product")], undefined);
213
+ let g = DomainGraph$ReventlessGwt.build([[
214
+ "Shop",
215
+ shop
216
+ ]]);
217
+ globalThis.expect(nodeKind(g, "Shop:ImportProduct")).toEqual("InboundTranslationSlice");
218
+ globalThis.expect(hasEdge(g, "Shop:ImportProduct", "Shop_Product_Add", "delegatesTo")).toBe(true);
219
+ });
220
+ globalThis.test("an outbound translation with a target routes to the command it raises", async () => {
221
+ let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Sync", "Shop_Product_Sync", undefined)], undefined, undefined, undefined, undefined)], undefined, [outbound("ExportProduct", ["Shop.Added"], ["ExportProduct.Sync"], "Product")], undefined, undefined);
222
+ let g = DomainGraph$ReventlessGwt.build([[
223
+ "Shop",
224
+ shop
225
+ ]]);
226
+ globalThis.expect(hasEdge(g, "Shop:ExportProduct", "Shop_Product_Sync", "delegatesTo")).toBe(true);
227
+ });
228
+ globalThis.test("an event produced by two write-sides is one node with two emit edges", async () => {
229
+ let shop = structure([
230
+ writable("A", undefined, ["Shop.Touched"], undefined, undefined, undefined),
231
+ writable("B", undefined, ["Shop.Touched"], undefined, undefined, undefined)
232
+ ], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
233
+ let g = DomainGraph$ReventlessGwt.build([[
234
+ "Shop",
235
+ shop
236
+ ]]);
237
+ globalThis.expect(g.nodes.filter(n => n.id === "Shop.Touched").length).toBe(1);
238
+ globalThis.expect(hasEdge(g, "Shop:A", "Shop.Touched", "emits")).toBe(true);
239
+ globalThis.expect(hasEdge(g, "Shop:B", "Shop.Touched", "emits")).toBe(true);
240
+ });
241
+ globalThis.test("an extension renders the cross-plugin event flow EP→event→Extension→delegate", async () => {
242
+ let extension_delegateNames = ["ProductDemand"];
243
+ let extension_eventTypes = ["Ordering.Orders.ItemOrdered"];
244
+ let extension_commandTypes = [];
245
+ let extension = {
246
+ name: "Ordering.Orders",
247
+ delegateNames: extension_delegateNames,
248
+ eventTypes: extension_eventTypes,
249
+ commandTypes: extension_commandTypes
250
+ };
251
+ let init = structure([writable("ProductDemand", undefined, ["Catalog.Recorded"], undefined, undefined, undefined)], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
252
+ let catalog_readModels = init.readModels;
253
+ let catalog_stateViewSlices = init.stateViewSlices;
254
+ let catalog_stateChangeSlices = init.stateChangeSlices;
255
+ let catalog_aggregates = init.aggregates;
256
+ let catalog_automationSlices = init.automationSlices;
257
+ let catalog_outboundTranslationSlices = init.outboundTranslationSlices;
258
+ let catalog_inboundTranslationSlices = init.inboundTranslationSlices;
259
+ let catalog_extensions = [extension];
260
+ let catalog_extensionPoints = init.extensionPoints;
261
+ let catalog = {
262
+ readModels: catalog_readModels,
263
+ stateViewSlices: catalog_stateViewSlices,
264
+ stateChangeSlices: catalog_stateChangeSlices,
265
+ aggregates: catalog_aggregates,
266
+ automationSlices: catalog_automationSlices,
267
+ outboundTranslationSlices: catalog_outboundTranslationSlices,
268
+ inboundTranslationSlices: catalog_inboundTranslationSlices,
269
+ extensions: catalog_extensions,
270
+ extensionPoints: catalog_extensionPoints
271
+ };
272
+ let g = DomainGraph$ReventlessGwt.build([[
273
+ "Catalog",
274
+ catalog
275
+ ]]);
276
+ globalThis.expect(nodeKind(g, "Ordering:ep:Ordering.Orders")).toEqual("ExtensionPoint");
277
+ globalThis.expect(hasEdge(g, "Ordering:ep:Ordering.Orders", "Ordering.Orders.ItemOrdered", "publishes")).toBe(true);
278
+ globalThis.expect(hasEdge(g, "Ordering.Orders.ItemOrdered", "Catalog:ext:Ordering.Orders", "consumes")).toBe(true);
279
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog:ProductDemand", "delegatesTo")).toBe(true);
280
+ let protoEvent = Stdlib_Option.getOrThrow(g.nodes.find(n => n.id === "Ordering.Orders.ItemOrdered"), undefined);
281
+ globalThis.expect(protoEvent.plugin).toBe("");
282
+ let ext = Stdlib_Option.getOrThrow(g.nodes.find(n => n.id === "Catalog:ext:Ordering.Orders"), undefined);
283
+ globalThis.expect(ext.label).toBe("Ordering.Orders.Catalog");
284
+ });
285
+ globalThis.test("an extension wires to the command(s) it creates, not straight to the delegate", async () => {
286
+ let extension_delegateNames = ["ProductDemand"];
287
+ let extension_eventTypes = ["Ordering.Orders.ItemOrdered"];
288
+ let extension_commandTypes = [];
289
+ let extension = {
290
+ name: "Ordering.Orders",
291
+ delegateNames: extension_delegateNames,
292
+ eventTypes: extension_eventTypes,
293
+ commandTypes: extension_commandTypes
294
+ };
295
+ let init = structure(undefined, undefined, undefined, [writable("ProductDemand", [command("RecordDemand", "Catalog_ProductDemand_RecordDemand", undefined)], ["Catalog.Recorded"], undefined, undefined, undefined)], undefined, undefined, undefined, undefined);
296
+ let catalog_readModels = init.readModels;
297
+ let catalog_stateViewSlices = init.stateViewSlices;
298
+ let catalog_stateChangeSlices = init.stateChangeSlices;
299
+ let catalog_aggregates = init.aggregates;
300
+ let catalog_automationSlices = init.automationSlices;
301
+ let catalog_outboundTranslationSlices = init.outboundTranslationSlices;
302
+ let catalog_inboundTranslationSlices = init.inboundTranslationSlices;
303
+ let catalog_extensions = [extension];
304
+ let catalog_extensionPoints = init.extensionPoints;
305
+ let catalog = {
306
+ readModels: catalog_readModels,
307
+ stateViewSlices: catalog_stateViewSlices,
308
+ stateChangeSlices: catalog_stateChangeSlices,
309
+ aggregates: catalog_aggregates,
310
+ automationSlices: catalog_automationSlices,
311
+ outboundTranslationSlices: catalog_outboundTranslationSlices,
312
+ inboundTranslationSlices: catalog_inboundTranslationSlices,
313
+ extensions: catalog_extensions,
314
+ extensionPoints: catalog_extensionPoints
315
+ };
316
+ let g = DomainGraph$ReventlessGwt.build([[
317
+ "Catalog",
318
+ catalog
319
+ ]]);
320
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog_ProductDemand_RecordDemand", "delegatesTo")).toBe(true);
321
+ globalThis.expect(hasEdge(g, "Catalog_ProductDemand_RecordDemand", "Catalog:ProductDemand", "handles")).toBe(true);
322
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog:ProductDemand", "delegatesTo")).toBe(false);
323
+ });
324
+ globalThis.test("an extension with no commands falls back to delegating to the write-side", async () => {
325
+ let extension_delegateNames = ["ProductDemand"];
326
+ let extension_eventTypes = ["Ordering.Orders.ItemOrdered"];
327
+ let extension_commandTypes = [];
328
+ let extension = {
329
+ name: "Ordering.Orders",
330
+ delegateNames: extension_delegateNames,
331
+ eventTypes: extension_eventTypes,
332
+ commandTypes: extension_commandTypes
333
+ };
334
+ let init = structure([writable("ProductDemand", undefined, ["Catalog.Recorded"], undefined, undefined, undefined)], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
335
+ let catalog_readModels = init.readModels;
336
+ let catalog_stateViewSlices = init.stateViewSlices;
337
+ let catalog_stateChangeSlices = init.stateChangeSlices;
338
+ let catalog_aggregates = init.aggregates;
339
+ let catalog_automationSlices = init.automationSlices;
340
+ let catalog_outboundTranslationSlices = init.outboundTranslationSlices;
341
+ let catalog_inboundTranslationSlices = init.inboundTranslationSlices;
342
+ let catalog_extensions = [extension];
343
+ let catalog_extensionPoints = init.extensionPoints;
344
+ let catalog = {
345
+ readModels: catalog_readModels,
346
+ stateViewSlices: catalog_stateViewSlices,
347
+ stateChangeSlices: catalog_stateChangeSlices,
348
+ aggregates: catalog_aggregates,
349
+ automationSlices: catalog_automationSlices,
350
+ outboundTranslationSlices: catalog_outboundTranslationSlices,
351
+ inboundTranslationSlices: catalog_inboundTranslationSlices,
352
+ extensions: catalog_extensions,
353
+ extensionPoints: catalog_extensionPoints
354
+ };
355
+ let g = DomainGraph$ReventlessGwt.build([[
356
+ "Catalog",
357
+ catalog
358
+ ]]);
359
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog:ProductDemand", "delegatesTo")).toBe(true);
360
+ });
361
+ globalThis.test("an owned extension point is fed by the producers of its source events", async () => {
362
+ let ep_delegateNames = ["CatalogDcbEventLog"];
363
+ let ep_sourceEventTypes = ["Catalog.ProductAdded"];
364
+ let ep_commandTypes = [];
365
+ let ep = {
366
+ name: "Catalog.Products",
367
+ delegateNames: ep_delegateNames,
368
+ sourceEventTypes: ep_sourceEventTypes,
369
+ commandTypes: ep_commandTypes
370
+ };
371
+ let catalog = structure(undefined, undefined, undefined, [writable("AddProduct", [command("AddProduct", "Catalog_AddProduct", undefined)], ["Catalog.ProductAdded"], undefined, undefined, undefined)], undefined, undefined, undefined, [ep]);
372
+ let g = DomainGraph$ReventlessGwt.build([[
373
+ "Catalog",
374
+ catalog
375
+ ]]);
376
+ globalThis.expect(nodeKind(g, "Catalog:ep:Catalog.Products")).toEqual("ExtensionPoint");
377
+ globalThis.expect(hasEdge(g, "Catalog:AddProduct", "Catalog.ProductAdded", "emits")).toBe(true);
378
+ globalThis.expect(hasEdge(g, "Catalog.ProductAdded", "Catalog:ep:Catalog.Products", "feeds")).toBe(true);
379
+ globalThis.expect(hasEdge(g, "Catalog:ep:Catalog.Products", "Catalog_AddProduct", "routesTo")).toBe(false);
380
+ });
381
+ globalThis.test("an extension point with an inbound command protocol routes to its delegate's commands", async () => {
382
+ let ep_delegateNames = ["AddProduct"];
383
+ let ep_sourceEventTypes = ["Catalog.ProductAdded"];
384
+ let ep_commandTypes = ["Catalog.Products.RequestAddProduct"];
385
+ let ep = {
386
+ name: "Catalog.Products",
387
+ delegateNames: ep_delegateNames,
388
+ sourceEventTypes: ep_sourceEventTypes,
389
+ commandTypes: ep_commandTypes
390
+ };
391
+ let catalog = structure(undefined, undefined, undefined, [writable("AddProduct", [command("AddProduct", "Catalog_AddProduct", undefined)], ["Catalog.ProductAdded"], undefined, undefined, undefined)], undefined, undefined, undefined, [ep]);
392
+ let g = DomainGraph$ReventlessGwt.build([[
393
+ "Catalog",
394
+ catalog
395
+ ]]);
396
+ globalThis.expect(hasEdge(g, "Catalog:ep:Catalog.Products", "Catalog_AddProduct", "routesTo")).toBe(true);
397
+ globalThis.expect(hasEdge(g, "Catalog_AddProduct", "Catalog:AddProduct", "handles")).toBe(true);
398
+ });
399
+ });
400
+
401
+ export {
402
+ command,
403
+ evt,
404
+ writable,
405
+ queryable,
406
+ automation,
407
+ inbound,
408
+ outbound,
409
+ structure,
410
+ hasEdge,
411
+ nodeKind,
412
+ }
413
+ /* Not a pure module */
@@ -0,0 +1,169 @@
1
+ // Worked example for Flow_GWT.AggregateCommandStep — two aggregate-style
2
+ // command steps threaded through one shared log. Verifies the framework's
3
+ // new aggregate-ID partitioning:
4
+ //
5
+ // - Two aggregates sequence cleanly when they share an ID.
6
+ // - A second aggregate's history is isolated from the first by `~id`, so a
7
+ // command on `o2` does not see `o1`'s prior events.
8
+ // - The `Error` branch flows through `thenError` like the slice form.
9
+
10
+ S.enableJson()
11
+
12
+ open Flow_GWT
13
+
14
+ // -- Aggregate: CatalogProduct (one event family, no error) -----------------
15
+
16
+ module CatalogProductAggregate = {
17
+ let name = "CatalogProduct"
18
+
19
+ @schema
20
+ type command =
21
+ | SyncNewProduct({productId: string, name: string})
22
+
23
+ @schema
24
+ type event =
25
+ | CatalogProductSynced({productId: string, name: string})
26
+
27
+ @schema
28
+ type error = AlreadySynced
29
+ }
30
+
31
+ module CatalogProductBehavior = {
32
+ module Spec = CatalogProductAggregate
33
+
34
+ type state = NotSynced | Synced
35
+
36
+ let initialState = NotSynced
37
+
38
+ let evolve = (_state, event: CatalogProductAggregate.event) =>
39
+ switch event {
40
+ | CatalogProductSynced(_) => Synced
41
+ }
42
+
43
+ let decide = (state, command: CatalogProductAggregate.command) =>
44
+ switch (state, command) {
45
+ | (NotSynced, SyncNewProduct({productId, name})) =>
46
+ Ok([CatalogProductAggregate.CatalogProductSynced({productId, name})])
47
+ | (Synced, _) => Error(CatalogProductAggregate.AlreadySynced)
48
+ }
49
+
50
+ let moduleUrl = "test://CatalogProductBehavior"
51
+ }
52
+
53
+ // -- Aggregate: Order ---------------------------------------------------------
54
+
55
+ module OrderAggregate = {
56
+ let name = "Order"
57
+
58
+ @schema
59
+ type command =
60
+ | Place({customerId: string, productIds: array<string>})
61
+ | Ship
62
+
63
+ @schema
64
+ type event =
65
+ | Placed({customerId: string, productIds: array<string>})
66
+ | Shipped
67
+
68
+ @schema
69
+ type error =
70
+ | AlreadyPlaced
71
+ | NotPlaced
72
+ }
73
+
74
+ module OrderBehavior = {
75
+ module Spec = OrderAggregate
76
+
77
+ type state = {placed: bool, shipped: bool}
78
+
79
+ let initialState = {placed: false, shipped: false}
80
+
81
+ let evolve = (state, event: OrderAggregate.event) =>
82
+ switch event {
83
+ | Placed(_) => {...state, placed: true}
84
+ | Shipped => {...state, shipped: true}
85
+ }
86
+
87
+ let decide = (state, command: OrderAggregate.command) =>
88
+ switch command {
89
+ | Place({customerId, productIds}) =>
90
+ state.placed
91
+ ? Error(OrderAggregate.AlreadyPlaced)
92
+ : Ok([OrderAggregate.Placed({customerId, productIds})])
93
+ | Ship =>
94
+ if !state.placed {
95
+ Error(OrderAggregate.NotPlaced)
96
+ } else if state.shipped {
97
+ Ok([])
98
+ } else {
99
+ Ok([OrderAggregate.Shipped])
100
+ }
101
+ }
102
+
103
+ let moduleUrl = "test://OrderBehavior"
104
+ }
105
+
106
+ // -- Step modules -------------------------------------------------------------
107
+
108
+ module Sync = AggregateCommandStep(CatalogProductAggregate, CatalogProductBehavior)
109
+ module Place = AggregateCommandStep(OrderAggregate, OrderBehavior)
110
+
111
+ // -- Flows --------------------------------------------------------------------
112
+
113
+ describe("Aggregate flow (single plugin)", () => {
114
+ test("sync product → place order → ship order, threaded through aggregate-ID", () =>
115
+ start
116
+ ->Sync.whenCommand(~id="p1", SyncNewProduct({productId: "p1", name: "Book"}))
117
+ ->Sync.thenEvent(CatalogProductSynced({productId: "p1", name: "Book"}))
118
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
119
+ ->Place.thenEvent(Placed({customerId: "c1", productIds: ["p1"]}))
120
+ ->Place.whenCommand(~id="o1", Ship)
121
+ ->Place.thenEvent(Shipped)
122
+ )
123
+
124
+ test("re-placing the same order is rejected (per-aggregate state replay)", () =>
125
+ start
126
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
127
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
128
+ ->Place.thenError(AlreadyPlaced)
129
+ )
130
+
131
+ test("a different order is not blocked by the first (~id isolation)", () =>
132
+ start
133
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
134
+ ->Place.thenEvent(Placed({customerId: "c1", productIds: ["p1"]}))
135
+ ->Place.whenCommand(~id="o2", Place({customerId: "c2", productIds: ["p2"]}))
136
+ ->Place.thenEvent(Placed({customerId: "c2", productIds: ["p2"]}))
137
+ )
138
+
139
+ test("ship before place returns NotPlaced", () =>
140
+ start
141
+ ->Place.whenCommand(~id="o1", Ship)
142
+ ->Place.thenError(NotPlaced)
143
+ )
144
+
145
+ test("givenEvents seeds history for an ~id starting mid-stream", () =>
146
+ start
147
+ ->Place.givenEvents(~id="o1", [Placed({customerId: "c1", productIds: ["p1"]})])
148
+ ->Place.whenCommand(~id="o1", Ship)
149
+ ->Place.thenEvent(Shipped)
150
+ )
151
+
152
+ test("ship after ship is idempotent (no events emitted)", () =>
153
+ start
154
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
155
+ ->Place.whenCommand(~id="o1", Ship)
156
+ ->Place.whenCommand(~id="o1", Ship)
157
+ ->Place.thenNoEvent
158
+ )
159
+
160
+ test("two independent aggregates evolve their own state in parallel", () =>
161
+ start
162
+ ->Sync.whenCommand(~id="p1", SyncNewProduct({productId: "p1", name: "Book"}))
163
+ ->Sync.thenEvent(CatalogProductSynced({productId: "p1", name: "Book"}))
164
+ ->Sync.whenCommand(~id="p2", SyncNewProduct({productId: "p2", name: "Pen"}))
165
+ ->Sync.thenEvent(CatalogProductSynced({productId: "p2", name: "Pen"}))
166
+ ->Sync.whenCommand(~id="p1", SyncNewProduct({productId: "p1", name: "Book"}))
167
+ ->Sync.thenError(AlreadySynced)
168
+ )
169
+ })