@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,505 @@
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_object from "@rescript/runtime/lib/es6/Primitive_object.js";
5
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
+ import * as DomainGraph$ReventlessGwt from "../src/DomainGraph.res.mjs";
7
+
8
+ function command(name, mutationField, apiExposedOpt) {
9
+ let apiExposed = apiExposedOpt !== undefined ? Primitive_option.valFromOption(apiExposedOpt) : undefined;
10
+ return {
11
+ name: name,
12
+ schema: "",
13
+ level: "Instance",
14
+ aggregateIdField: undefined,
15
+ mutationField: mutationField,
16
+ references: [],
17
+ allowedStates: undefined,
18
+ apiExposed: apiExposed
19
+ };
20
+ }
21
+
22
+ function evt(name) {
23
+ return {
24
+ name: name,
25
+ schema: "",
26
+ references: []
27
+ };
28
+ }
29
+
30
+ function writable(name, commandsOpt, producesOpt, consumesOpt, linkedViewsOpt, eventsOpt, chapterOpt) {
31
+ let commands = commandsOpt !== undefined ? commandsOpt : [];
32
+ let produces = producesOpt !== undefined ? producesOpt : [];
33
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
34
+ let linkedViews = linkedViewsOpt !== undefined ? linkedViewsOpt : [];
35
+ let events = eventsOpt !== undefined ? eventsOpt : [];
36
+ let chapter = chapterOpt !== undefined ? Primitive_option.valFromOption(chapterOpt) : undefined;
37
+ return {
38
+ name: name,
39
+ commands: commands,
40
+ producedEventTypes: produces,
41
+ consumedEventTypes: consumes,
42
+ linkedViews: linkedViews,
43
+ consistencyRead: undefined,
44
+ events: events,
45
+ chapter: chapter
46
+ };
47
+ }
48
+
49
+ function queryable(name, consumesOpt, visibilityOpt, chapterOpt) {
50
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
51
+ let visibility = visibilityOpt !== undefined ? Primitive_option.valFromOption(visibilityOpt) : undefined;
52
+ let chapter = chapterOpt !== undefined ? Primitive_option.valFromOption(chapterOpt) : undefined;
53
+ return {
54
+ name: name,
55
+ queryField: "",
56
+ schema: "",
57
+ consumedEventTypes: consumes,
58
+ linkedWriteSide: [],
59
+ labelField: "id",
60
+ searchableFields: [],
61
+ statusField: undefined,
62
+ visibility: visibility,
63
+ chapter: chapter
64
+ };
65
+ }
66
+
67
+ function automation(name, consumesOpt, producesOpt, targetOpt, chapterOpt) {
68
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
69
+ let produces = producesOpt !== undefined ? producesOpt : [];
70
+ let target = targetOpt !== undefined ? targetOpt : "";
71
+ let chapter = chapterOpt !== undefined ? Primitive_option.valFromOption(chapterOpt) : undefined;
72
+ return {
73
+ name: name,
74
+ consumedEventTypes: consumes,
75
+ producedCommandTypes: produces,
76
+ targetName: target,
77
+ chapter: chapter
78
+ };
79
+ }
80
+
81
+ function inbound(name, producesOpt, targetOpt, extOpt, chapterOpt) {
82
+ let produces = producesOpt !== undefined ? producesOpt : [];
83
+ let target = targetOpt !== undefined ? targetOpt : "";
84
+ let ext = extOpt !== undefined ? Primitive_option.valFromOption(extOpt) : undefined;
85
+ let chapter = chapterOpt !== undefined ? Primitive_option.valFromOption(chapterOpt) : undefined;
86
+ return {
87
+ name: name,
88
+ commandTypes: produces,
89
+ targetName: target,
90
+ externalSystem: ext,
91
+ chapter: chapter
92
+ };
93
+ }
94
+
95
+ function outbound(name, consumesOpt, producesOpt, targetOpt, extOpt, chapterOpt) {
96
+ let consumes = consumesOpt !== undefined ? consumesOpt : [];
97
+ let produces = producesOpt !== undefined ? producesOpt : [];
98
+ let target = targetOpt !== undefined ? Primitive_option.valFromOption(targetOpt) : undefined;
99
+ let ext = extOpt !== undefined ? Primitive_option.valFromOption(extOpt) : undefined;
100
+ let chapter = chapterOpt !== undefined ? Primitive_option.valFromOption(chapterOpt) : undefined;
101
+ return {
102
+ name: name,
103
+ consumedEventTypes: consumes,
104
+ inboundCommandTypes: produces,
105
+ targetName: target,
106
+ externalSystem: ext,
107
+ chapter: chapter
108
+ };
109
+ }
110
+
111
+ function structure(aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionPointsOpt) {
112
+ let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
113
+ let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
114
+ let stateViewSlices = stateViewSlicesOpt !== undefined ? stateViewSlicesOpt : [];
115
+ let stateChangeSlices = stateChangeSlicesOpt !== undefined ? stateChangeSlicesOpt : [];
116
+ let automationSlices = automationSlicesOpt !== undefined ? automationSlicesOpt : [];
117
+ let outboundTranslationSlices = outboundTranslationSlicesOpt !== undefined ? outboundTranslationSlicesOpt : [];
118
+ let inboundTranslationSlices = inboundTranslationSlicesOpt !== undefined ? inboundTranslationSlicesOpt : [];
119
+ let extensionPoints = extensionPointsOpt !== undefined ? extensionPointsOpt : [];
120
+ return {
121
+ readModels: readModels,
122
+ stateViewSlices: stateViewSlices,
123
+ stateChangeSlices: stateChangeSlices,
124
+ aggregates: aggregates,
125
+ automationSlices: automationSlices,
126
+ outboundTranslationSlices: outboundTranslationSlices,
127
+ inboundTranslationSlices: inboundTranslationSlices,
128
+ extensions: [],
129
+ extensionPoints: extensionPoints
130
+ };
131
+ }
132
+
133
+ function hasEdge(g, from, to, kind) {
134
+ return g.edges.some(e => {
135
+ if (e.from === from && e.to === to) {
136
+ return Primitive_object.equal(e.kind, kind);
137
+ } else {
138
+ return false;
139
+ }
140
+ });
141
+ }
142
+
143
+ function edgeLabel(g, from, to, kind) {
144
+ return Stdlib_Option.flatMap(g.edges.find(e => {
145
+ if (e.from === from && e.to === to) {
146
+ return Primitive_object.equal(e.kind, kind);
147
+ } else {
148
+ return false;
149
+ }
150
+ }), e => e.label);
151
+ }
152
+
153
+ function nodeKind(g, id) {
154
+ return Stdlib_Option.map(g.nodes.find(n => n.id === id), n => n.kind);
155
+ }
156
+
157
+ globalThis.describe("DomainGraph.build", () => {
158
+ globalThis.test("Command → Aggregate → Event → ReadModel (DCB consumedEventTypes)", async () => {
159
+ let shop = structure([writable("Order", [command("Place", "Shop_Order_Place", undefined)], ["Shop.Placed"], undefined, undefined, undefined, undefined)], undefined, [queryable("OrderView", ["Shop.Placed"], undefined, undefined)], undefined, undefined, undefined, undefined, undefined);
160
+ let g = DomainGraph$ReventlessGwt.build([[
161
+ "Shop",
162
+ shop
163
+ ]]);
164
+ globalThis.expect(nodeKind(g, "Shop_Order_Place")).toEqual("Command");
165
+ globalThis.expect(nodeKind(g, "Shop:Order")).toEqual("Aggregate");
166
+ globalThis.expect(nodeKind(g, "Shop.Placed")).toEqual("Event");
167
+ globalThis.expect(nodeKind(g, "Shop:OrderView")).toEqual("StateViewSlice");
168
+ globalThis.expect(hasEdge(g, "Shop_Order_Place", "Shop:Order", "Handles")).toBe(true);
169
+ globalThis.expect(hasEdge(g, "Shop:Order", "Shop.Placed", "Emits")).toBe(true);
170
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:OrderView", "Projects")).toBe(true);
171
+ });
172
+ globalThis.test("payload-less events (in `events`, not producedEventTypes) still get an emitted node", async () => {
173
+ let shop = structure([writable("Category", undefined, ["Catalog.Added"], undefined, undefined, [
174
+ {
175
+ name: "Added",
176
+ schema: "",
177
+ references: []
178
+ },
179
+ {
180
+ name: "Archived",
181
+ schema: "",
182
+ references: []
183
+ }
184
+ ], undefined)], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
185
+ let g = DomainGraph$ReventlessGwt.build([[
186
+ "Catalog",
187
+ shop
188
+ ]]);
189
+ globalThis.expect(nodeKind(g, "Catalog.Added")).toEqual("Event");
190
+ globalThis.expect(nodeKind(g, "Catalog.Archived")).toEqual("Event");
191
+ globalThis.expect(hasEdge(g, "Catalog:Category", "Catalog.Added", "Emits")).toBe(true);
192
+ globalThis.expect(hasEdge(g, "Catalog:Category", "Catalog.Archived", "Emits")).toBe(true);
193
+ });
194
+ globalThis.test("a payload-less event also projects into the aggregate's linked view", async () => {
195
+ let shop = structure([writable("Category", undefined, ["Catalog.Added"], undefined, ["Categories"], [
196
+ {
197
+ name: "Added",
198
+ schema: "",
199
+ references: []
200
+ },
201
+ {
202
+ name: "Archived",
203
+ schema: "",
204
+ references: []
205
+ }
206
+ ], undefined)], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
207
+ let g = DomainGraph$ReventlessGwt.build([[
208
+ "Catalog",
209
+ shop
210
+ ]]);
211
+ globalThis.expect(hasEdge(g, "Catalog.Added", "Catalog:Categories", "Projects")).toBe(true);
212
+ globalThis.expect(hasEdge(g, "Catalog.Archived", "Catalog:Categories", "Projects")).toBe(true);
213
+ });
214
+ globalThis.test("an Internal StateViewSlice still gets a node + projects edge (dev graph shows it)", async () => {
215
+ let shop = structure([writable("Order", undefined, ["Shop.Placed"], undefined, undefined, undefined, undefined)], undefined, [queryable("AvailableView", ["Shop.Placed"], "Internal", undefined)], undefined, undefined, undefined, undefined, undefined);
216
+ let g = DomainGraph$ReventlessGwt.build([[
217
+ "Shop",
218
+ shop
219
+ ]]);
220
+ globalThis.expect(nodeKind(g, "Shop:AvailableView")).toEqual("StateViewSlice");
221
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:AvailableView", "Projects")).toBe(true);
222
+ });
223
+ globalThis.test("classic linkedViews draws each produced event into the linked read model", async () => {
224
+ let shop = structure([writable("Order", undefined, [
225
+ "Shop.Placed",
226
+ "Shop.Shipped"
227
+ ], undefined, ["Orders"], undefined, undefined)], [queryable("Orders", undefined, undefined, undefined)], undefined, undefined, undefined, undefined, undefined, undefined);
228
+ let g = DomainGraph$ReventlessGwt.build([[
229
+ "Shop",
230
+ shop
231
+ ]]);
232
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:Orders", "Projects")).toBe(true);
233
+ globalThis.expect(hasEdge(g, "Shop.Shipped", "Shop:Orders", "Projects")).toBe(true);
234
+ });
235
+ globalThis.test("automation slices are triggered by their consumed events", async () => {
236
+ let shop = structure([writable("Order", undefined, ["Shop.Placed"], undefined, undefined, undefined, undefined)], undefined, undefined, undefined, [automation("Notify", ["Shop.Placed"], undefined, undefined, undefined)], undefined, undefined, undefined);
237
+ let g = DomainGraph$ReventlessGwt.build([[
238
+ "Shop",
239
+ shop
240
+ ]]);
241
+ globalThis.expect(nodeKind(g, "Shop:Notify")).toEqual("AutomationSlice");
242
+ globalThis.expect(hasEdge(g, "Shop.Placed", "Shop:Notify", "Triggers")).toBe(true);
243
+ });
244
+ globalThis.test("an automation routes to the specific command it raises on its target", async () => {
245
+ let shop = structure(undefined, undefined, undefined, [writable("Ship", [
246
+ command("ShipOrder", "Shop_Ship_ShipOrder", undefined),
247
+ command("Place", "Shop_Ship_Place", undefined)
248
+ ], ["Shop.Shipped"], undefined, undefined, undefined, undefined)], [automation("AutoShip", ["Shop.Placed"], ["AutoShip.ShipOrder"], "Ship", undefined)], undefined, undefined, undefined);
249
+ let g = DomainGraph$ReventlessGwt.build([[
250
+ "Shop",
251
+ shop
252
+ ]]);
253
+ globalThis.expect(hasEdge(g, "Shop:AutoShip", "Shop_Ship_ShipOrder", "DelegatesTo")).toBe(true);
254
+ globalThis.expect(hasEdge(g, "Shop:AutoShip", "Shop_Ship_Place", "DelegatesTo")).toBe(false);
255
+ });
256
+ globalThis.test("an inbound translation routes to the command it raises on its target", async () => {
257
+ let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Add", "Shop_Product_Add", undefined)], undefined, undefined, undefined, undefined, undefined)], undefined, undefined, [inbound("ImportProduct", ["ImportProduct.Add"], "Product", undefined, undefined)], undefined);
258
+ let g = DomainGraph$ReventlessGwt.build([[
259
+ "Shop",
260
+ shop
261
+ ]]);
262
+ globalThis.expect(nodeKind(g, "Shop:ImportProduct")).toEqual("InboundTranslationSlice");
263
+ globalThis.expect(hasEdge(g, "Shop:ImportProduct", "Shop_Product_Add", "DelegatesTo")).toBe(true);
264
+ });
265
+ globalThis.test("an outbound translation with a target routes to the command it raises", async () => {
266
+ let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Sync", "Shop_Product_Sync", undefined)], undefined, undefined, undefined, undefined, undefined)], undefined, [outbound("ExportProduct", ["Shop.Added"], ["ExportProduct.Sync"], "Product", undefined, undefined)], undefined, undefined);
267
+ let g = DomainGraph$ReventlessGwt.build([[
268
+ "Shop",
269
+ shop
270
+ ]]);
271
+ globalThis.expect(hasEdge(g, "Shop:ExportProduct", "Shop_Product_Sync", "DelegatesTo")).toBe(true);
272
+ });
273
+ globalThis.test("an inbound translation naming an external system draws a box feeding it", async () => {
274
+ let shop = structure(undefined, undefined, undefined, undefined, undefined, undefined, [inbound("ImportProduct", ["ImportProduct.Add"], "Product", "SupplierFeed", undefined)], undefined);
275
+ let g = DomainGraph$ReventlessGwt.build([[
276
+ "Shop",
277
+ shop
278
+ ]]);
279
+ globalThis.expect(nodeKind(g, "ext:SupplierFeed")).toEqual("ExternalSystem");
280
+ globalThis.expect(Stdlib_Option.map(g.nodes.find(n => n.id === "ext:SupplierFeed"), n => n.plugin)).toEqual("");
281
+ globalThis.expect(hasEdge(g, "ext:SupplierFeed", "Shop:ImportProduct", "TranslatesIn")).toBe(true);
282
+ globalThis.expect(edgeLabel(g, "ext:SupplierFeed", "Shop:ImportProduct", "TranslatesIn")).toEqual(undefined);
283
+ });
284
+ globalThis.test("an outbound translation naming an external system feeds the box", async () => {
285
+ let shop = structure(undefined, undefined, undefined, undefined, undefined, [outbound("SendEmail", ["Shop.Placed"], undefined, Primitive_option.some(undefined), "EmailService", undefined)], undefined, undefined);
286
+ let g = DomainGraph$ReventlessGwt.build([[
287
+ "Shop",
288
+ shop
289
+ ]]);
290
+ globalThis.expect(nodeKind(g, "ext:EmailService")).toEqual("ExternalSystem");
291
+ globalThis.expect(hasEdge(g, "Shop:SendEmail", "ext:EmailService", "TranslatesOut")).toBe(true);
292
+ globalThis.expect(edgeLabel(g, "Shop:SendEmail", "ext:EmailService", "TranslatesOut")).toEqual(undefined);
293
+ });
294
+ globalThis.test("translation slices with no external system draw no box (opt-in)", async () => {
295
+ let shop = structure(undefined, undefined, undefined, undefined, undefined, undefined, [inbound("ImportProduct", undefined, "Product", undefined, undefined)], undefined);
296
+ let g = DomainGraph$ReventlessGwt.build([[
297
+ "Shop",
298
+ shop
299
+ ]]);
300
+ globalThis.expect(g.nodes.some(n => n.kind === "ExternalSystem")).toBe(false);
301
+ });
302
+ globalThis.test("two slices naming the same external system share one deduped box", async () => {
303
+ let a = structure(undefined, undefined, undefined, undefined, undefined, [outbound("SendEmailA", ["A.X"], undefined, Primitive_option.some(undefined), "EmailService", undefined)], undefined, undefined);
304
+ let b = structure(undefined, undefined, undefined, undefined, undefined, undefined, [inbound("FromEmail", ["FromEmail.Record"], "Inbox", "EmailService", undefined)], undefined);
305
+ let g = DomainGraph$ReventlessGwt.build([
306
+ [
307
+ "PluginA",
308
+ a
309
+ ],
310
+ [
311
+ "PluginB",
312
+ b
313
+ ]
314
+ ]);
315
+ globalThis.expect(g.nodes.filter(n => n.id === "ext:EmailService").length).toBe(1);
316
+ globalThis.expect(hasEdge(g, "PluginA:SendEmailA", "ext:EmailService", "TranslatesOut")).toBe(true);
317
+ globalThis.expect(hasEdge(g, "ext:EmailService", "PluginB:FromEmail", "TranslatesIn")).toBe(true);
318
+ });
319
+ globalThis.test("an event produced by two write-sides is one node with two emit edges", async () => {
320
+ let shop = structure([
321
+ writable("A", undefined, ["Shop.Touched"], undefined, undefined, undefined, undefined),
322
+ writable("B", undefined, ["Shop.Touched"], undefined, undefined, undefined, undefined)
323
+ ], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
324
+ let g = DomainGraph$ReventlessGwt.build([[
325
+ "Shop",
326
+ shop
327
+ ]]);
328
+ globalThis.expect(g.nodes.filter(n => n.id === "Shop.Touched").length).toBe(1);
329
+ globalThis.expect(hasEdge(g, "Shop:A", "Shop.Touched", "Emits")).toBe(true);
330
+ globalThis.expect(hasEdge(g, "Shop:B", "Shop.Touched", "Emits")).toBe(true);
331
+ });
332
+ globalThis.test("an extension renders the cross-plugin event flow EP→event→Extension→delegate", async () => {
333
+ let extension_delegateNames = ["ProductDemand"];
334
+ let extension_eventTypes = ["Ordering.Orders.ItemOrdered"];
335
+ let extension_commandTypes = [];
336
+ let extension = {
337
+ name: "Ordering.Orders",
338
+ delegateNames: extension_delegateNames,
339
+ eventTypes: extension_eventTypes,
340
+ commandTypes: extension_commandTypes
341
+ };
342
+ let init = structure([writable("ProductDemand", undefined, ["Catalog.Recorded"], undefined, undefined, undefined, undefined)], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
343
+ let catalog_readModels = init.readModels;
344
+ let catalog_stateViewSlices = init.stateViewSlices;
345
+ let catalog_stateChangeSlices = init.stateChangeSlices;
346
+ let catalog_aggregates = init.aggregates;
347
+ let catalog_automationSlices = init.automationSlices;
348
+ let catalog_outboundTranslationSlices = init.outboundTranslationSlices;
349
+ let catalog_inboundTranslationSlices = init.inboundTranslationSlices;
350
+ let catalog_extensions = [extension];
351
+ let catalog_extensionPoints = init.extensionPoints;
352
+ let catalog = {
353
+ readModels: catalog_readModels,
354
+ stateViewSlices: catalog_stateViewSlices,
355
+ stateChangeSlices: catalog_stateChangeSlices,
356
+ aggregates: catalog_aggregates,
357
+ automationSlices: catalog_automationSlices,
358
+ outboundTranslationSlices: catalog_outboundTranslationSlices,
359
+ inboundTranslationSlices: catalog_inboundTranslationSlices,
360
+ extensions: catalog_extensions,
361
+ extensionPoints: catalog_extensionPoints
362
+ };
363
+ let g = DomainGraph$ReventlessGwt.build([[
364
+ "Catalog",
365
+ catalog
366
+ ]]);
367
+ globalThis.expect(nodeKind(g, "Ordering:ep:Ordering.Orders")).toEqual("ExtensionPoint");
368
+ globalThis.expect(hasEdge(g, "Ordering:ep:Ordering.Orders", "Ordering.Orders.ItemOrdered", "Publishes")).toBe(true);
369
+ globalThis.expect(hasEdge(g, "Ordering.Orders.ItemOrdered", "Catalog:ext:Ordering.Orders", "Consumes")).toBe(true);
370
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog:ProductDemand", "DelegatesTo")).toBe(true);
371
+ let protoEvent = Stdlib_Option.getOrThrow(g.nodes.find(n => n.id === "Ordering.Orders.ItemOrdered"), undefined);
372
+ globalThis.expect(protoEvent.plugin).toBe("");
373
+ let ext = Stdlib_Option.getOrThrow(g.nodes.find(n => n.id === "Catalog:ext:Ordering.Orders"), undefined);
374
+ globalThis.expect(ext.label).toBe("Ordering.Orders.Catalog");
375
+ });
376
+ globalThis.test("an extension wires to the command(s) it creates, not straight to the delegate", async () => {
377
+ let extension_delegateNames = ["ProductDemand"];
378
+ let extension_eventTypes = ["Ordering.Orders.ItemOrdered"];
379
+ let extension_commandTypes = [];
380
+ let extension = {
381
+ name: "Ordering.Orders",
382
+ delegateNames: extension_delegateNames,
383
+ eventTypes: extension_eventTypes,
384
+ commandTypes: extension_commandTypes
385
+ };
386
+ let init = structure(undefined, undefined, undefined, [writable("ProductDemand", [command("RecordDemand", "Catalog_ProductDemand_RecordDemand", undefined)], ["Catalog.Recorded"], undefined, undefined, undefined, undefined)], undefined, undefined, undefined, undefined);
387
+ let catalog_readModels = init.readModels;
388
+ let catalog_stateViewSlices = init.stateViewSlices;
389
+ let catalog_stateChangeSlices = init.stateChangeSlices;
390
+ let catalog_aggregates = init.aggregates;
391
+ let catalog_automationSlices = init.automationSlices;
392
+ let catalog_outboundTranslationSlices = init.outboundTranslationSlices;
393
+ let catalog_inboundTranslationSlices = init.inboundTranslationSlices;
394
+ let catalog_extensions = [extension];
395
+ let catalog_extensionPoints = init.extensionPoints;
396
+ let catalog = {
397
+ readModels: catalog_readModels,
398
+ stateViewSlices: catalog_stateViewSlices,
399
+ stateChangeSlices: catalog_stateChangeSlices,
400
+ aggregates: catalog_aggregates,
401
+ automationSlices: catalog_automationSlices,
402
+ outboundTranslationSlices: catalog_outboundTranslationSlices,
403
+ inboundTranslationSlices: catalog_inboundTranslationSlices,
404
+ extensions: catalog_extensions,
405
+ extensionPoints: catalog_extensionPoints
406
+ };
407
+ let g = DomainGraph$ReventlessGwt.build([[
408
+ "Catalog",
409
+ catalog
410
+ ]]);
411
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog_ProductDemand_RecordDemand", "DelegatesTo")).toBe(true);
412
+ globalThis.expect(hasEdge(g, "Catalog_ProductDemand_RecordDemand", "Catalog:ProductDemand", "Handles")).toBe(true);
413
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog:ProductDemand", "DelegatesTo")).toBe(false);
414
+ });
415
+ globalThis.test("an extension with no commands falls back to delegating to the write-side", async () => {
416
+ let extension_delegateNames = ["ProductDemand"];
417
+ let extension_eventTypes = ["Ordering.Orders.ItemOrdered"];
418
+ let extension_commandTypes = [];
419
+ let extension = {
420
+ name: "Ordering.Orders",
421
+ delegateNames: extension_delegateNames,
422
+ eventTypes: extension_eventTypes,
423
+ commandTypes: extension_commandTypes
424
+ };
425
+ let init = structure([writable("ProductDemand", undefined, ["Catalog.Recorded"], undefined, undefined, undefined, undefined)], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
426
+ let catalog_readModels = init.readModels;
427
+ let catalog_stateViewSlices = init.stateViewSlices;
428
+ let catalog_stateChangeSlices = init.stateChangeSlices;
429
+ let catalog_aggregates = init.aggregates;
430
+ let catalog_automationSlices = init.automationSlices;
431
+ let catalog_outboundTranslationSlices = init.outboundTranslationSlices;
432
+ let catalog_inboundTranslationSlices = init.inboundTranslationSlices;
433
+ let catalog_extensions = [extension];
434
+ let catalog_extensionPoints = init.extensionPoints;
435
+ let catalog = {
436
+ readModels: catalog_readModels,
437
+ stateViewSlices: catalog_stateViewSlices,
438
+ stateChangeSlices: catalog_stateChangeSlices,
439
+ aggregates: catalog_aggregates,
440
+ automationSlices: catalog_automationSlices,
441
+ outboundTranslationSlices: catalog_outboundTranslationSlices,
442
+ inboundTranslationSlices: catalog_inboundTranslationSlices,
443
+ extensions: catalog_extensions,
444
+ extensionPoints: catalog_extensionPoints
445
+ };
446
+ let g = DomainGraph$ReventlessGwt.build([[
447
+ "Catalog",
448
+ catalog
449
+ ]]);
450
+ globalThis.expect(hasEdge(g, "Catalog:ext:Ordering.Orders", "Catalog:ProductDemand", "DelegatesTo")).toBe(true);
451
+ });
452
+ globalThis.test("an owned extension point is fed by the producers of its source events", async () => {
453
+ let ep_delegateNames = ["CatalogDcbEventLog"];
454
+ let ep_sourceEventTypes = ["Catalog.ProductAdded"];
455
+ let ep_commandTypes = [];
456
+ let ep = {
457
+ name: "Catalog.Products",
458
+ delegateNames: ep_delegateNames,
459
+ sourceEventTypes: ep_sourceEventTypes,
460
+ commandTypes: ep_commandTypes
461
+ };
462
+ let catalog = structure(undefined, undefined, undefined, [writable("AddProduct", [command("AddProduct", "Catalog_AddProduct", undefined)], ["Catalog.ProductAdded"], undefined, undefined, undefined, undefined)], undefined, undefined, undefined, [ep]);
463
+ let g = DomainGraph$ReventlessGwt.build([[
464
+ "Catalog",
465
+ catalog
466
+ ]]);
467
+ globalThis.expect(nodeKind(g, "Catalog:ep:Catalog.Products")).toEqual("ExtensionPoint");
468
+ globalThis.expect(hasEdge(g, "Catalog:AddProduct", "Catalog.ProductAdded", "Emits")).toBe(true);
469
+ globalThis.expect(hasEdge(g, "Catalog.ProductAdded", "Catalog:ep:Catalog.Products", "Feeds")).toBe(true);
470
+ globalThis.expect(hasEdge(g, "Catalog:ep:Catalog.Products", "Catalog_AddProduct", "RoutesTo")).toBe(false);
471
+ });
472
+ globalThis.test("an extension point with an inbound command protocol routes to its delegate's commands", async () => {
473
+ let ep_delegateNames = ["AddProduct"];
474
+ let ep_sourceEventTypes = ["Catalog.ProductAdded"];
475
+ let ep_commandTypes = ["Catalog.Products.RequestAddProduct"];
476
+ let ep = {
477
+ name: "Catalog.Products",
478
+ delegateNames: ep_delegateNames,
479
+ sourceEventTypes: ep_sourceEventTypes,
480
+ commandTypes: ep_commandTypes
481
+ };
482
+ let catalog = structure(undefined, undefined, undefined, [writable("AddProduct", [command("AddProduct", "Catalog_AddProduct", undefined)], ["Catalog.ProductAdded"], undefined, undefined, undefined, undefined)], undefined, undefined, undefined, [ep]);
483
+ let g = DomainGraph$ReventlessGwt.build([[
484
+ "Catalog",
485
+ catalog
486
+ ]]);
487
+ globalThis.expect(hasEdge(g, "Catalog:ep:Catalog.Products", "Catalog_AddProduct", "RoutesTo")).toBe(true);
488
+ globalThis.expect(hasEdge(g, "Catalog_AddProduct", "Catalog:AddProduct", "Handles")).toBe(true);
489
+ });
490
+ });
491
+
492
+ export {
493
+ command,
494
+ evt,
495
+ writable,
496
+ queryable,
497
+ automation,
498
+ inbound,
499
+ outbound,
500
+ structure,
501
+ hasEdge,
502
+ edgeLabel,
503
+ nodeKind,
504
+ }
505
+ /* Not a pure module */
@@ -0,0 +1,171 @@
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
+ let snapshot = None
38
+
39
+ let evolve = (_state, event: CatalogProductAggregate.event) =>
40
+ switch event {
41
+ | CatalogProductSynced(_) => Synced
42
+ }
43
+
44
+ let decide = (state, command: CatalogProductAggregate.command) =>
45
+ switch (state, command) {
46
+ | (NotSynced, SyncNewProduct({productId, name})) =>
47
+ Ok([CatalogProductAggregate.CatalogProductSynced({productId, name})])
48
+ | (Synced, _) => Error(CatalogProductAggregate.AlreadySynced)
49
+ }
50
+
51
+ let moduleUrl = "test://CatalogProductBehavior"
52
+ }
53
+
54
+ // -- Aggregate: Order ---------------------------------------------------------
55
+
56
+ module OrderAggregate = {
57
+ let name = "Order"
58
+
59
+ @schema
60
+ type command =
61
+ | Place({customerId: string, productIds: array<string>})
62
+ | Ship
63
+
64
+ @schema
65
+ type event =
66
+ | Placed({customerId: string, productIds: array<string>})
67
+ | Shipped
68
+
69
+ @schema
70
+ type error =
71
+ | AlreadyPlaced
72
+ | NotPlaced
73
+ }
74
+
75
+ module OrderBehavior = {
76
+ module Spec = OrderAggregate
77
+
78
+ type state = {placed: bool, shipped: bool}
79
+
80
+ let initialState = {placed: false, shipped: false}
81
+ let snapshot = None
82
+
83
+ let evolve = (state, event: OrderAggregate.event) =>
84
+ switch event {
85
+ | Placed(_) => {...state, placed: true}
86
+ | Shipped => {...state, shipped: true}
87
+ }
88
+
89
+ let decide = (state, command: OrderAggregate.command) =>
90
+ switch command {
91
+ | Place({customerId, productIds}) =>
92
+ state.placed
93
+ ? Error(OrderAggregate.AlreadyPlaced)
94
+ : Ok([OrderAggregate.Placed({customerId, productIds})])
95
+ | Ship =>
96
+ if !state.placed {
97
+ Error(OrderAggregate.NotPlaced)
98
+ } else if state.shipped {
99
+ Ok([])
100
+ } else {
101
+ Ok([OrderAggregate.Shipped])
102
+ }
103
+ }
104
+
105
+ let moduleUrl = "test://OrderBehavior"
106
+ }
107
+
108
+ // -- Step modules -------------------------------------------------------------
109
+
110
+ module Sync = AggregateCommandStep(CatalogProductAggregate, CatalogProductBehavior)
111
+ module Place = AggregateCommandStep(OrderAggregate, OrderBehavior)
112
+
113
+ // -- Flows --------------------------------------------------------------------
114
+
115
+ describe("Aggregate flow (single plugin)", () => {
116
+ test("sync product → place order → ship order, threaded through aggregate-ID", () =>
117
+ start
118
+ ->Sync.whenCommand(~id="p1", SyncNewProduct({productId: "p1", name: "Book"}))
119
+ ->Sync.thenEvent(CatalogProductSynced({productId: "p1", name: "Book"}))
120
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
121
+ ->Place.thenEvent(Placed({customerId: "c1", productIds: ["p1"]}))
122
+ ->Place.whenCommand(~id="o1", Ship)
123
+ ->Place.thenEvent(Shipped)
124
+ )
125
+
126
+ test("re-placing the same order is rejected (per-aggregate state replay)", () =>
127
+ start
128
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
129
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
130
+ ->Place.thenError(AlreadyPlaced)
131
+ )
132
+
133
+ test("a different order is not blocked by the first (~id isolation)", () =>
134
+ start
135
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
136
+ ->Place.thenEvent(Placed({customerId: "c1", productIds: ["p1"]}))
137
+ ->Place.whenCommand(~id="o2", Place({customerId: "c2", productIds: ["p2"]}))
138
+ ->Place.thenEvent(Placed({customerId: "c2", productIds: ["p2"]}))
139
+ )
140
+
141
+ test("ship before place returns NotPlaced", () =>
142
+ start
143
+ ->Place.whenCommand(~id="o1", Ship)
144
+ ->Place.thenError(NotPlaced)
145
+ )
146
+
147
+ test("givenEvents seeds history for an ~id starting mid-stream", () =>
148
+ start
149
+ ->Place.givenEvents(~id="o1", [Placed({customerId: "c1", productIds: ["p1"]})])
150
+ ->Place.whenCommand(~id="o1", Ship)
151
+ ->Place.thenEvent(Shipped)
152
+ )
153
+
154
+ test("ship after ship is idempotent (no events emitted)", () =>
155
+ start
156
+ ->Place.whenCommand(~id="o1", Place({customerId: "c1", productIds: ["p1"]}))
157
+ ->Place.whenCommand(~id="o1", Ship)
158
+ ->Place.whenCommand(~id="o1", Ship)
159
+ ->Place.thenNoEvent
160
+ )
161
+
162
+ test("two independent aggregates evolve their own state in parallel", () =>
163
+ start
164
+ ->Sync.whenCommand(~id="p1", SyncNewProduct({productId: "p1", name: "Book"}))
165
+ ->Sync.thenEvent(CatalogProductSynced({productId: "p1", name: "Book"}))
166
+ ->Sync.whenCommand(~id="p2", SyncNewProduct({productId: "p2", name: "Pen"}))
167
+ ->Sync.thenEvent(CatalogProductSynced({productId: "p2", name: "Pen"}))
168
+ ->Sync.whenCommand(~id="p1", SyncNewProduct({productId: "p1", name: "Book"}))
169
+ ->Sync.thenError(AlreadySynced)
170
+ )
171
+ })