@reventlessdev/reventless-gwt 1.0.0-alpha.100

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 (184) hide show
  1. package/CHANGELOG.md +916 -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 +569 -0
  10. package/src/Behavior_GWT.res.mjs +523 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +147 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +37 -0
  16. package/src/Cancellation.res.mjs +46 -0
  17. package/src/ChildProcess.res +102 -0
  18. package/src/ChildProcess.res.mjs +107 -0
  19. package/src/Cli.res +905 -0
  20. package/src/Cli.res.mjs +952 -0
  21. package/src/Collector.res +212 -0
  22. package/src/Collector.res.mjs +218 -0
  23. package/src/ComponentMeta.res +77 -0
  24. package/src/ComponentMeta.res.mjs +91 -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 +99 -0
  33. package/src/DomainDeadCode.res +98 -0
  34. package/src/DomainDeadCode.res.mjs +72 -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 +45 -0
  40. package/src/Filter.res.mjs +47 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +109 -0
  44. package/src/FormatterHuman.res.mjs +115 -0
  45. package/src/FormatterJson.res +264 -0
  46. package/src/FormatterJson.res.mjs +322 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +100 -0
  50. package/src/FormatterTap.res.mjs +131 -0
  51. package/src/FormatterVsCode.res +352 -0
  52. package/src/FormatterVsCode.res.mjs +546 -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 +119 -0
  62. package/src/LocalHost.res.mjs +99 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MismatchRender.res +72 -0
  66. package/src/MismatchRender.res.mjs +127 -0
  67. package/src/MultiSourceProjection_GWT.res +316 -0
  68. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  69. package/src/OutboundTranslation_GWT.res +238 -0
  70. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  71. package/src/Outcome.res +97 -0
  72. package/src/Outcome.res.mjs +93 -0
  73. package/src/PackageScan.res +80 -0
  74. package/src/PackageScan.res.mjs +81 -0
  75. package/src/PlatformRunner.res +181 -0
  76. package/src/PlatformRunner.res.mjs +176 -0
  77. package/src/PlatformScan.res +142 -0
  78. package/src/PlatformScan.res.mjs +132 -0
  79. package/src/ProcessManager.res +135 -0
  80. package/src/ProcessManager.res.mjs +119 -0
  81. package/src/Projection_GWT.res +312 -0
  82. package/src/Projection_GWT.res.mjs +332 -0
  83. package/src/Query_GWT.res +379 -0
  84. package/src/Query_GWT.res.mjs +367 -0
  85. package/src/RenderRescript.res +125 -0
  86. package/src/RenderRescript.res.mjs +141 -0
  87. package/src/RunWorker.res +24 -0
  88. package/src/RunWorker.res.mjs +16 -0
  89. package/src/RunnerTypes.res +68 -0
  90. package/src/RunnerTypes.res.mjs +60 -0
  91. package/src/SideEffect_GWT.res +157 -0
  92. package/src/SideEffect_GWT.res.mjs +113 -0
  93. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  94. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  95. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  96. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  97. package/src/StateChange/WithFixtures.res +22 -0
  98. package/src/StateChange/WithFixtures.res.mjs +31 -0
  99. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  100. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  101. package/src/StateChange/WithManualOpen.res +23 -0
  102. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  103. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  104. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  105. package/src/StubRuntime.res +54 -0
  106. package/src/StubRuntime.res.mjs +55 -0
  107. package/src/TestFixtures.res +16 -0
  108. package/src/TestFixtures.res.mjs +34 -0
  109. package/src/Watch.res +108 -0
  110. package/src/Watch.res.mjs +85 -0
  111. package/src/WatcherProbe.res +56 -0
  112. package/src/WatcherProbe.res.mjs +59 -0
  113. package/src/Worker.res +24 -0
  114. package/src/Worker.res.mjs +9 -0
  115. package/tests/AutomationGwtTest.res +71 -0
  116. package/tests/AutomationGwtTest.res.mjs +189 -0
  117. package/tests/BuildClassifierTest.res +133 -0
  118. package/tests/BuildClassifierTest.res.mjs +176 -0
  119. package/tests/CliRunEntryTest.res +57 -0
  120. package/tests/CliRunEntryTest.res.mjs +65 -0
  121. package/tests/CliWatchScopeTest.res +62 -0
  122. package/tests/CliWatchScopeTest.res.mjs +95 -0
  123. package/tests/ComponentMetaTest.res +67 -0
  124. package/tests/ComponentMetaTest.res.mjs +71 -0
  125. package/tests/DelegateGwtTest.res +487 -0
  126. package/tests/DelegateGwtTest.res.mjs +891 -0
  127. package/tests/DiscoveryTest.res +42 -0
  128. package/tests/DiscoveryTest.res.mjs +43 -0
  129. package/tests/DomainDeadCodeTest.res +119 -0
  130. package/tests/DomainDeadCodeTest.res.mjs +158 -0
  131. package/tests/DomainGraphTest.res +448 -0
  132. package/tests/DomainGraphTest.res.mjs +505 -0
  133. package/tests/FlowAggregateGwtTest.res +171 -0
  134. package/tests/FlowAggregateGwtTest.res.mjs +273 -0
  135. package/tests/FlowCrossPluginGwtTest.res +340 -0
  136. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  137. package/tests/FlowGwtTest.res +222 -0
  138. package/tests/FlowGwtTest.res.mjs +433 -0
  139. package/tests/FormatterGoldenTest.res +125 -0
  140. package/tests/FormatterGoldenTest.res.mjs +162 -0
  141. package/tests/FormatterVsCodeMessageTest.res +41 -0
  142. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  143. package/tests/InboundTranslationGwtTest.res +34 -0
  144. package/tests/InboundTranslationGwtTest.res.mjs +99 -0
  145. package/tests/LocalHostIntegration.res +70 -0
  146. package/tests/LocalHostIntegration.res.mjs +64 -0
  147. package/tests/LocalHostTest.res +71 -0
  148. package/tests/LocalHostTest.res.mjs +63 -0
  149. package/tests/MappingGwtTest.res +350 -0
  150. package/tests/MappingGwtTest.res.mjs +596 -0
  151. package/tests/OutboundTranslationGwtTest.res +59 -0
  152. package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
  153. package/tests/PlatformRunnerTest.res +66 -0
  154. package/tests/PlatformRunnerTest.res.mjs +85 -0
  155. package/tests/PlatformScanTest.res +61 -0
  156. package/tests/PlatformScanTest.res.mjs +56 -0
  157. package/tests/QueryGwtTest.res +161 -0
  158. package/tests/QueryGwtTest.res.mjs +314 -0
  159. package/tests/QueryResolverGwtTest.res +71 -0
  160. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  161. package/tests/RunnerUnitTest.res +219 -0
  162. package/tests/RunnerUnitTest.res.mjs +244 -0
  163. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  164. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  165. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  166. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  167. package/tests/SideEffect/MockEmail.res +18 -0
  168. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  169. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  170. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  171. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  172. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  173. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  174. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  175. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  176. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  177. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  178. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  179. package/tests/StateChangeSliceGwtTest.res +160 -0
  180. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  181. package/tests/StateViewSliceGwtTest.res +64 -0
  182. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  183. package/tests/WatchDebounceTest.res +106 -0
  184. package/tests/WatchDebounceTest.res.mjs +128 -0
@@ -0,0 +1,273 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Flow_GWT$ReventlessGwt from "../src/Flow_GWT.res.mjs";
5
+
6
+ S.enableJson();
7
+
8
+ let commandSchema = S.schema(s => ({
9
+ TAG: "SyncNewProduct",
10
+ productId: s.m(S.string),
11
+ name: s.m(S.string)
12
+ }));
13
+
14
+ let eventSchema = S.schema(s => ({
15
+ TAG: "CatalogProductSynced",
16
+ productId: s.m(S.string),
17
+ name: s.m(S.string)
18
+ }));
19
+
20
+ let errorSchema = S.literal("AlreadySynced");
21
+
22
+ function commandAuthorization(param) {
23
+ return "AllowAuthenticated";
24
+ }
25
+
26
+ let CatalogProductAggregate = {
27
+ name: "CatalogProduct",
28
+ commandSchema: commandSchema,
29
+ eventSchema: eventSchema,
30
+ errorSchema: errorSchema,
31
+ commandAuthorization: commandAuthorization
32
+ };
33
+
34
+ function evolve(_state, event) {
35
+ return "Synced";
36
+ }
37
+
38
+ function decide(state, command) {
39
+ if (state === "NotSynced") {
40
+ return {
41
+ TAG: "Ok",
42
+ _0: [{
43
+ TAG: "CatalogProductSynced",
44
+ productId: command.productId,
45
+ name: command.name
46
+ }]
47
+ };
48
+ } else {
49
+ return {
50
+ TAG: "Error",
51
+ _0: "AlreadySynced"
52
+ };
53
+ }
54
+ }
55
+
56
+ let moduleUrl = "test://CatalogProductBehavior";
57
+
58
+ let CatalogProductBehavior = {
59
+ Spec: undefined,
60
+ initialState: "NotSynced",
61
+ snapshot: undefined,
62
+ evolve: evolve,
63
+ decide: decide,
64
+ moduleUrl: moduleUrl
65
+ };
66
+
67
+ let commandSchema$1 = S.union([
68
+ S.schema(s => ({
69
+ TAG: "Place",
70
+ customerId: s.m(S.string),
71
+ productIds: s.m(S.array(S.string))
72
+ })),
73
+ S.literal("Ship")
74
+ ]);
75
+
76
+ let eventSchema$1 = S.union([
77
+ S.schema(s => ({
78
+ TAG: "Placed",
79
+ customerId: s.m(S.string),
80
+ productIds: s.m(S.array(S.string))
81
+ })),
82
+ S.literal("Shipped")
83
+ ]);
84
+
85
+ let errorSchema$1 = S.union([
86
+ S.literal("AlreadyPlaced"),
87
+ S.literal("NotPlaced")
88
+ ]);
89
+
90
+ function commandAuthorization$1(param) {
91
+ return "AllowAuthenticated";
92
+ }
93
+
94
+ let OrderAggregate = {
95
+ name: "Order",
96
+ commandSchema: commandSchema$1,
97
+ eventSchema: eventSchema$1,
98
+ errorSchema: errorSchema$1,
99
+ commandAuthorization: commandAuthorization$1
100
+ };
101
+
102
+ let initialState = {
103
+ placed: false,
104
+ shipped: false
105
+ };
106
+
107
+ function evolve$1(state, event) {
108
+ if (typeof event !== "object") {
109
+ return {
110
+ placed: state.placed,
111
+ shipped: true
112
+ };
113
+ } else {
114
+ return {
115
+ placed: true,
116
+ shipped: state.shipped
117
+ };
118
+ }
119
+ }
120
+
121
+ function decide$1(state, command) {
122
+ if (typeof command !== "object") {
123
+ if (state.placed) {
124
+ if (state.shipped) {
125
+ return {
126
+ TAG: "Ok",
127
+ _0: []
128
+ };
129
+ } else {
130
+ return {
131
+ TAG: "Ok",
132
+ _0: ["Shipped"]
133
+ };
134
+ }
135
+ } else {
136
+ return {
137
+ TAG: "Error",
138
+ _0: "NotPlaced"
139
+ };
140
+ }
141
+ } else if (state.placed) {
142
+ return {
143
+ TAG: "Error",
144
+ _0: "AlreadyPlaced"
145
+ };
146
+ } else {
147
+ return {
148
+ TAG: "Ok",
149
+ _0: [{
150
+ TAG: "Placed",
151
+ customerId: command.customerId,
152
+ productIds: command.productIds
153
+ }]
154
+ };
155
+ }
156
+ }
157
+
158
+ let moduleUrl$1 = "test://OrderBehavior";
159
+
160
+ let OrderBehavior = {
161
+ Spec: undefined,
162
+ initialState: initialState,
163
+ snapshot: undefined,
164
+ evolve: evolve$1,
165
+ decide: decide$1,
166
+ moduleUrl: moduleUrl$1
167
+ };
168
+
169
+ let Sync = Flow_GWT$ReventlessGwt.AggregateCommandStep(CatalogProductAggregate)({
170
+ Spec: CatalogProductAggregate,
171
+ initialState: "NotSynced",
172
+ evolve: evolve,
173
+ decide: decide,
174
+ snapshot: undefined,
175
+ moduleUrl: moduleUrl
176
+ });
177
+
178
+ let Place = Flow_GWT$ReventlessGwt.AggregateCommandStep(OrderAggregate)({
179
+ Spec: OrderAggregate,
180
+ initialState: initialState,
181
+ evolve: evolve$1,
182
+ decide: decide$1,
183
+ snapshot: undefined,
184
+ moduleUrl: moduleUrl$1
185
+ });
186
+
187
+ Flow_GWT$ReventlessGwt.describe("Aggregate flow (single plugin)", () => {
188
+ Flow_GWT$ReventlessGwt.test("sync product → place order → ship order, threaded through aggregate-ID", undefined, () => Place.thenEvent(Place.whenCommand(Place.thenEvent(Place.whenCommand(Sync.thenEvent(Sync.whenCommand(Flow_GWT$ReventlessGwt.start, "p1", {
189
+ TAG: "SyncNewProduct",
190
+ productId: "p1",
191
+ name: "Book"
192
+ }), {
193
+ TAG: "CatalogProductSynced",
194
+ productId: "p1",
195
+ name: "Book"
196
+ }), "o1", {
197
+ TAG: "Place",
198
+ customerId: "c1",
199
+ productIds: ["p1"]
200
+ }), {
201
+ TAG: "Placed",
202
+ customerId: "c1",
203
+ productIds: ["p1"]
204
+ }), "o1", "Ship"), "Shipped"));
205
+ Flow_GWT$ReventlessGwt.test("re-placing the same order is rejected (per-aggregate state replay)", undefined, () => Place.thenError(Place.whenCommand(Place.whenCommand(Flow_GWT$ReventlessGwt.start, "o1", {
206
+ TAG: "Place",
207
+ customerId: "c1",
208
+ productIds: ["p1"]
209
+ }), "o1", {
210
+ TAG: "Place",
211
+ customerId: "c1",
212
+ productIds: ["p1"]
213
+ }), "AlreadyPlaced"));
214
+ Flow_GWT$ReventlessGwt.test("a different order is not blocked by the first (~id isolation)", undefined, () => Place.thenEvent(Place.whenCommand(Place.thenEvent(Place.whenCommand(Flow_GWT$ReventlessGwt.start, "o1", {
215
+ TAG: "Place",
216
+ customerId: "c1",
217
+ productIds: ["p1"]
218
+ }), {
219
+ TAG: "Placed",
220
+ customerId: "c1",
221
+ productIds: ["p1"]
222
+ }), "o2", {
223
+ TAG: "Place",
224
+ customerId: "c2",
225
+ productIds: ["p2"]
226
+ }), {
227
+ TAG: "Placed",
228
+ customerId: "c2",
229
+ productIds: ["p2"]
230
+ }));
231
+ Flow_GWT$ReventlessGwt.test("ship before place returns NotPlaced", undefined, () => Place.thenError(Place.whenCommand(Flow_GWT$ReventlessGwt.start, "o1", "Ship"), "NotPlaced"));
232
+ Flow_GWT$ReventlessGwt.test("givenEvents seeds history for an ~id starting mid-stream", undefined, () => Place.thenEvent(Place.whenCommand(Place.givenEvents(Flow_GWT$ReventlessGwt.start, "o1", [{
233
+ TAG: "Placed",
234
+ customerId: "c1",
235
+ productIds: ["p1"]
236
+ }]), "o1", "Ship"), "Shipped"));
237
+ Flow_GWT$ReventlessGwt.test("ship after ship is idempotent (no events emitted)", undefined, () => Place.thenNoEvent(Place.whenCommand(Place.whenCommand(Place.whenCommand(Flow_GWT$ReventlessGwt.start, "o1", {
238
+ TAG: "Place",
239
+ customerId: "c1",
240
+ productIds: ["p1"]
241
+ }), "o1", "Ship"), "o1", "Ship")));
242
+ Flow_GWT$ReventlessGwt.test("two independent aggregates evolve their own state in parallel", undefined, () => Sync.thenError(Sync.whenCommand(Sync.thenEvent(Sync.whenCommand(Sync.thenEvent(Sync.whenCommand(Flow_GWT$ReventlessGwt.start, "p1", {
243
+ TAG: "SyncNewProduct",
244
+ productId: "p1",
245
+ name: "Book"
246
+ }), {
247
+ TAG: "CatalogProductSynced",
248
+ productId: "p1",
249
+ name: "Book"
250
+ }), "p2", {
251
+ TAG: "SyncNewProduct",
252
+ productId: "p2",
253
+ name: "Pen"
254
+ }), {
255
+ TAG: "CatalogProductSynced",
256
+ productId: "p2",
257
+ name: "Pen"
258
+ }), "p1", {
259
+ TAG: "SyncNewProduct",
260
+ productId: "p1",
261
+ name: "Book"
262
+ }), "AlreadySynced"));
263
+ });
264
+
265
+ export {
266
+ CatalogProductAggregate,
267
+ CatalogProductBehavior,
268
+ OrderAggregate,
269
+ OrderBehavior,
270
+ Sync,
271
+ Place,
272
+ }
273
+ /* Not a pure module */
@@ -0,0 +1,340 @@
1
+ // Worked example for cross-plugin Flow_GWT — two self-contained "plugins"
2
+ // (Catalog and Ordering) joined by their ExtensionPoint mapping + Extension
3
+ // delegate, threaded through one shared log.
4
+ //
5
+ // Catalog: AddProduct ─→ ProductAdded
6
+ // └─[Products EP mapping]→ ProductBecameAvailable
7
+ // └─[Ordering Products extension]→ SyncProduct
8
+ // └─ Ordering: SyncProduct ─→ ProductSynced
9
+ // Ordering: PlaceOrder ─→ OrderPlaced ← succeeds only because of the sync
10
+ // └─[Orders EP mapping, fan-out]→ ItemOrdered ×N
11
+ // └─[Catalog demand extension]→ RecordDemand ×N
12
+ //
13
+ // Proves the property no single-component test can: a product added in Catalog
14
+ // becomes orderable in Ordering only via the cross-plugin sync, and a batch
15
+ // order fans out into one demand command per product across the boundary.
16
+ // See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phase 3.
17
+
18
+ S.enableJson()
19
+
20
+ open Flow_GWT
21
+
22
+ module EPM = ReventlessInfra.ExtensionPointMapping
23
+ module EM = ReventlessInfra.ExtensionMapping
24
+
25
+ // ===========================================================================
26
+ // Catalog plugin
27
+ // ===========================================================================
28
+
29
+ module AddProductSlice = {
30
+ let name = "AddProduct"
31
+
32
+ @schema
33
+ type consumedEvent = ProductAdded({productId: @s.matches(Reventless.DcbTag.string) string})
34
+
35
+ @schema
36
+ type command =
37
+ AddProduct({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
38
+
39
+ @schema
40
+ type error = AlreadyAdded
41
+
42
+ @schema
43
+ type event =
44
+ ProductAdded({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
45
+ }
46
+
47
+ module AddProductBehavior = {
48
+ module Spec = AddProductSlice
49
+ type state = {added: bool}
50
+ let initialState = {added: false}
51
+ let evolve = (_state, event: AddProductSlice.consumedEvent) =>
52
+ switch event {
53
+ | ProductAdded(_) => {added: true}
54
+ }
55
+ let decide = (state, command: AddProductSlice.command) =>
56
+ switch command {
57
+ | AddProduct({productId, name, price}) =>
58
+ state.added
59
+ ? Error(AddProductSlice.AlreadyAdded)
60
+ : Ok([AddProductSlice.ProductAdded({productId, name, price})])
61
+ }
62
+ }
63
+
64
+ // Catalog → Ordering extension point (product availability).
65
+ module ProductsEpSpec = {
66
+ let name = "Catalog.Products"
67
+ let moduleUrl = ""
68
+ @schema type command = NoCommand
69
+ @schema
70
+ type event = ProductBecameAvailable({productId: string, name: string, price: float})
71
+ @schema type directive = NoDirective
72
+ }
73
+
74
+ module ProductDelegate = {
75
+ module Id = Reventless.Id.StringPure
76
+ let name = "Product"
77
+ @schema type command = NoCommand
78
+ @schema type event = ProductAdded({productId: string, name: string, price: float})
79
+ @schema type error = NoError
80
+ let moduleUrl = ""
81
+ let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
82
+ }
83
+
84
+ module ProductsEpMapping = {
85
+ module ExtensionPoint = ProductsEpSpec
86
+ module Delegate = ProductDelegate
87
+ let moduleUrl = "test:FlowCrossPluginGwtTest:ProductsEpMapping"
88
+ let mapIncomingCommand = (_id, _command, _meta) => []
89
+ let mapOutgoingEvent = Some(
90
+ (_id, event: ProductDelegate.event, _meta, _q) =>
91
+ switch event {
92
+ | ProductAdded({productId, name, price}) => [
93
+ EPM.PublishEvent(productId, ProductsEpSpec.ProductBecameAvailable({productId, name, price})),
94
+ ]
95
+ },
96
+ )
97
+ }
98
+
99
+ // Ordering → Catalog extension point (per-product order demand).
100
+ module OrdersEpSpec = {
101
+ let name = "Ordering.Orders"
102
+ let moduleUrl = ""
103
+ @schema type command = NoCommand
104
+ @schema type event = ItemOrdered({productId: string, orderId: string})
105
+ @schema type directive = NoDirective
106
+ }
107
+
108
+ module OrderDelegate = {
109
+ module Id = Reventless.Id.StringPure
110
+ let name = "Order"
111
+ @schema type command = NoCommand
112
+ @schema type event = OrderPlaced({orderId: string, productIds: array<string>})
113
+ @schema type error = NoError
114
+ let moduleUrl = ""
115
+ let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
116
+ }
117
+
118
+ module OrdersEpMapping = {
119
+ module ExtensionPoint = OrdersEpSpec
120
+ module Delegate = OrderDelegate
121
+ let moduleUrl = "test:FlowCrossPluginGwtTest:OrdersEpMapping"
122
+ let mapIncomingCommand = (_id, _command, _meta) => []
123
+ let mapOutgoingEvent = Some(
124
+ (_id, event: OrderDelegate.event, _meta, _q) =>
125
+ switch event {
126
+ | OrderPlaced({orderId, productIds}) =>
127
+ productIds->Array.map(pid =>
128
+ EPM.PublishEvent(pid, OrdersEpSpec.ItemOrdered({productId: pid, orderId}))
129
+ )
130
+ },
131
+ )
132
+ }
133
+
134
+ // Catalog's RecordDemand slice — the delegate Catalog's Orders extension drives.
135
+ module RecordDemandSlice = {
136
+ module Id = Reventless.Id.StringPure
137
+ let name = "RecordDemand"
138
+ @schema
139
+ type consumedEvent =
140
+ DemandRecorded({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
141
+ @schema
142
+ type command =
143
+ RecordDemand({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
144
+ @schema type error = NoError
145
+ @schema
146
+ type event =
147
+ DemandRecorded({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
148
+ let moduleUrl = ""
149
+ let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
150
+ }
151
+
152
+ module RecordDemandBehavior = {
153
+ module Spec = RecordDemandSlice
154
+ type state = {count: int}
155
+ let initialState = {count: 0}
156
+ let evolve = (state, event: RecordDemandSlice.consumedEvent) =>
157
+ switch event {
158
+ | DemandRecorded(_) => {count: state.count + 1}
159
+ }
160
+ let decide = (_state, command: RecordDemandSlice.command) =>
161
+ switch command {
162
+ | RecordDemand({productId, orderId}) => Ok([RecordDemandSlice.DemandRecorded({productId, orderId})])
163
+ }
164
+ }
165
+
166
+ module OrdersExtMapping = {
167
+ module ExtensionPoint = OrdersEpSpec
168
+ module Delegate = RecordDemandSlice
169
+ let moduleUrl = ""
170
+ let delegateModuleUrl = ""
171
+ let mapIncomingEvent = (_id, event: OrdersEpSpec.event, _meta, _pd, _q) =>
172
+ switch event {
173
+ | ItemOrdered({productId, orderId}) => [
174
+ EM.PublishStateChangeSliceCommand(RecordDemandSlice.RecordDemand({productId, orderId})),
175
+ ]
176
+ }
177
+ let mapOutgoingEvent = None
178
+ }
179
+
180
+ // ===========================================================================
181
+ // Ordering plugin
182
+ // ===========================================================================
183
+
184
+ // Local shadow of catalog products, kept in sync via the extension.
185
+ module SyncProductSlice = {
186
+ module Id = Reventless.Id.StringPure
187
+ let name = "SyncProduct"
188
+ @schema
189
+ type consumedEvent = ProductSynced({productId: @s.matches(Reventless.DcbTag.string) string})
190
+ @schema
191
+ type command =
192
+ SyncProduct({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
193
+ @schema type error = NoError
194
+ @schema
195
+ type event =
196
+ ProductSynced({productId: @s.matches(Reventless.DcbTag.string) string, name: string, price: float})
197
+ let moduleUrl = ""
198
+ let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
199
+ }
200
+
201
+ module SyncProductBehavior = {
202
+ module Spec = SyncProductSlice
203
+ type state = {synced: bool}
204
+ let initialState = {synced: false}
205
+ let evolve = (_state, event: SyncProductSlice.consumedEvent) =>
206
+ switch event {
207
+ | ProductSynced(_) => {synced: true}
208
+ }
209
+ let decide = (_state, command: SyncProductSlice.command) =>
210
+ switch command {
211
+ | SyncProduct({productId, name, price}) =>
212
+ Ok([SyncProductSlice.ProductSynced({productId, name, price})])
213
+ }
214
+ }
215
+
216
+ module ProductsExtMapping = {
217
+ module ExtensionPoint = ProductsEpSpec
218
+ module Delegate = SyncProductSlice
219
+ let moduleUrl = ""
220
+ let delegateModuleUrl = ""
221
+ let mapIncomingEvent = (_id, event: ProductsEpSpec.event, _meta, _pd, _q) =>
222
+ switch event {
223
+ | ProductBecameAvailable({productId, name, price}) => [
224
+ EM.PublishStateChangeSliceCommand(SyncProductSlice.SyncProduct({productId, name, price})),
225
+ ]
226
+ }
227
+ let mapOutgoingEvent = None
228
+ }
229
+
230
+ module PlaceOrderSlice = {
231
+ let name = "PlaceOrder"
232
+ @schema
233
+ type consumedEvent =
234
+ | OrderPlaced({orderId: @s.matches(Reventless.DcbTag.string) string})
235
+ | ProductSynced({productId: @s.matches(Reventless.DcbTag.string) string})
236
+ @schema
237
+ type command =
238
+ PlaceOrder({
239
+ orderId: @s.matches(Reventless.DcbTag.string) string,
240
+ productIds: array<@s.matches(Reventless.DcbTag.stringForKey(~key="productId")) string>,
241
+ })
242
+ @schema
243
+ type error =
244
+ | OrderAlreadyPlaced
245
+ | ProductsNotAvailable({missing: array<string>})
246
+ @schema
247
+ type event =
248
+ OrderPlaced({
249
+ orderId: @s.matches(Reventless.DcbTag.string) string,
250
+ productIds: array<@s.matches(Reventless.DcbTag.stringForKey(~key="productId")) string>,
251
+ })
252
+ }
253
+
254
+ module PlaceOrderBehavior = {
255
+ module Spec = PlaceOrderSlice
256
+ type state = {placed: array<string>, available: array<string>}
257
+ let initialState = {placed: [], available: []}
258
+ let evolve = (state, event: PlaceOrderSlice.consumedEvent) =>
259
+ switch event {
260
+ | OrderPlaced({orderId}) => {...state, placed: state.placed->Array.concat([orderId])}
261
+ | ProductSynced({productId}) => {...state, available: state.available->Array.concat([productId])}
262
+ }
263
+ let decide = (state, command: PlaceOrderSlice.command) =>
264
+ switch command {
265
+ | PlaceOrder({orderId, productIds}) =>
266
+ if state.placed->Array.includes(orderId) {
267
+ Error(PlaceOrderSlice.OrderAlreadyPlaced)
268
+ } else {
269
+ let missing = productIds->Array.filter(p => !(state.available->Array.includes(p)))
270
+ missing->Array.length > 0
271
+ ? Error(PlaceOrderSlice.ProductsNotAvailable({missing: missing}))
272
+ : Ok([PlaceOrderSlice.OrderPlaced({orderId, productIds})])
273
+ }
274
+ }
275
+ }
276
+
277
+ // ===========================================================================
278
+ // Step modules
279
+ // ===========================================================================
280
+
281
+ module AddProduct = CommandStep(AddProductSlice, AddProductBehavior)
282
+ module ProductsEp = ExtensionPointStep(ProductsEpMapping)
283
+ module ProductsExt = ExtensionStep(ProductsExtMapping)
284
+ module Sync = CommandStep(SyncProductSlice, SyncProductBehavior)
285
+ module Place = CommandStep(PlaceOrderSlice, PlaceOrderBehavior)
286
+ module OrdersEp = ExtensionPointStep(OrdersEpMapping)
287
+ module OrdersExt = ExtensionStep(OrdersExtMapping)
288
+ module Demand = CommandStep(RecordDemandSlice, RecordDemandBehavior)
289
+
290
+ // ===========================================================================
291
+ // Flows
292
+ // ===========================================================================
293
+
294
+ describe("Cross-plugin flow", () => {
295
+ test("Tier 2 — a product added in Catalog becomes orderable in Ordering via sync", () =>
296
+ start
297
+ ->AddProduct.whenCommand(AddProductSlice.AddProduct({productId: "p1", name: "Book", price: 9.99}))
298
+ ->AddProduct.thenEvent(AddProductSlice.ProductAdded({productId: "p1", name: "Book", price: 9.99}))
299
+ ->ProductsEp.whenPublishedThrough
300
+ ->ProductsEp.thenPublicEvent(
301
+ ProductsEpSpec.ProductBecameAvailable({productId: "p1", name: "Book", price: 9.99}),
302
+ )
303
+ ->ProductsExt.whenExtensionReacts
304
+ ->ProductsExt.thenIssuesCommand(
305
+ SyncProductSlice.SyncProduct({productId: "p1", name: "Book", price: 9.99}),
306
+ )
307
+ ->Sync.whenCommand(SyncProductSlice.SyncProduct({productId: "p1", name: "Book", price: 9.99}))
308
+ ->Sync.thenEvent(SyncProductSlice.ProductSynced({productId: "p1", name: "Book", price: 9.99}))
309
+ ->Place.whenCommand(PlaceOrderSlice.PlaceOrder({orderId: "o1", productIds: ["p1"]}))
310
+ ->Place.thenEvent(PlaceOrderSlice.OrderPlaced({orderId: "o1", productIds: ["p1"]}))
311
+ )
312
+
313
+ test("Tier 2 — placing an order without the cross-plugin sync is rejected", () =>
314
+ start
315
+ ->Place.whenCommand(PlaceOrderSlice.PlaceOrder({orderId: "o1", productIds: ["p1"]}))
316
+ ->Place.thenError(ProductsNotAvailable({missing: ["p1"]}))
317
+ )
318
+
319
+ test("Tier 3 — a batch order fans out to one demand command per product across the boundary", () =>
320
+ start
321
+ ->Sync.givenEvents([
322
+ SyncProductSlice.ProductSynced({productId: "p1", name: "Book", price: 9.99}),
323
+ SyncProductSlice.ProductSynced({productId: "p2", name: "Pen", price: 1.5}),
324
+ ])
325
+ ->Place.whenCommand(PlaceOrderSlice.PlaceOrder({orderId: "o1", productIds: ["p1", "p2"]}))
326
+ ->Place.thenEvent(PlaceOrderSlice.OrderPlaced({orderId: "o1", productIds: ["p1", "p2"]}))
327
+ ->OrdersEp.whenPublishedThrough
328
+ ->OrdersEp.thenPublicEvents([
329
+ OrdersEpSpec.ItemOrdered({productId: "p1", orderId: "o1"}),
330
+ OrdersEpSpec.ItemOrdered({productId: "p2", orderId: "o1"}),
331
+ ])
332
+ ->OrdersExt.whenExtensionReacts
333
+ ->OrdersExt.thenIssuesCommands([
334
+ RecordDemandSlice.RecordDemand({productId: "p1", orderId: "o1"}),
335
+ RecordDemandSlice.RecordDemand({productId: "p2", orderId: "o1"}),
336
+ ])
337
+ ->Demand.whenCommand(RecordDemandSlice.RecordDemand({productId: "p1", orderId: "o1"}))
338
+ ->Demand.thenEvent(RecordDemandSlice.DemandRecorded({productId: "p1", orderId: "o1"}))
339
+ )
340
+ })