@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.
- package/CHANGELOG.md +698 -0
- package/LICENSE +202 -0
- package/bin/reventless-dev.mjs +21 -0
- package/package.json +54 -0
- package/rescript.json +36 -0
- package/src/.gwtignore +10 -0
- package/src/Automation_GWT.res +207 -0
- package/src/Automation_GWT.res.mjs +160 -0
- package/src/Behavior_GWT.res +603 -0
- package/src/Behavior_GWT.res.mjs +426 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +137 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +31 -0
- package/src/Cancellation.res.mjs +45 -0
- package/src/ChildProcess.res +84 -0
- package/src/ChildProcess.res.mjs +92 -0
- package/src/Cli.res +671 -0
- package/src/Cli.res.mjs +754 -0
- package/src/Collector.res +201 -0
- package/src/Collector.res.mjs +215 -0
- package/src/ComponentMeta.res +104 -0
- package/src/ComponentMeta.res.mjs +120 -0
- package/src/ComponentScan.res +103 -0
- package/src/ComponentScan.res.mjs +105 -0
- package/src/Delegate_GWT.res +303 -0
- package/src/Delegate_GWT.res.mjs +372 -0
- package/src/Diff.res +91 -0
- package/src/Diff.res.mjs +98 -0
- package/src/Discovery.res +105 -0
- package/src/Discovery.res.mjs +103 -0
- package/src/DomainDeadCode.res +93 -0
- package/src/DomainDeadCode.res.mjs +69 -0
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/EventMapping_GWT.res +29 -0
- package/src/EventMapping_GWT.res.mjs +44 -0
- package/src/Filter.res +36 -0
- package/src/Filter.res.mjs +40 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +138 -0
- package/src/FormatterHuman.res.mjs +122 -0
- package/src/FormatterJson.res +257 -0
- package/src/FormatterJson.res.mjs +320 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +139 -0
- package/src/FormatterTap.res.mjs +149 -0
- package/src/FormatterVsCode.res +362 -0
- package/src/FormatterVsCode.res.mjs +564 -0
- package/src/Hint.res +119 -0
- package/src/Hint.res.mjs +124 -0
- package/src/InboundTranslation_GWT.res +83 -0
- package/src/InboundTranslation_GWT.res.mjs +94 -0
- package/src/JestBind.res +70 -0
- package/src/JestBind.res.mjs +63 -0
- package/src/Loader.res +24 -0
- package/src/Loader.res.mjs +23 -0
- package/src/LocalHost.res +135 -0
- package/src/LocalHost.res.mjs +127 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MultiSourceProjection_GWT.res +316 -0
- package/src/MultiSourceProjection_GWT.res.mjs +354 -0
- package/src/OutboundTranslation_GWT.res +238 -0
- package/src/OutboundTranslation_GWT.res.mjs +186 -0
- package/src/Outcome.res +161 -0
- package/src/Outcome.res.mjs +140 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +160 -0
- package/src/PlatformRunner.res.mjs +155 -0
- package/src/PlatformScan.res +138 -0
- package/src/PlatformScan.res.mjs +131 -0
- package/src/ProcessManager.res +91 -0
- package/src/ProcessManager.res.mjs +80 -0
- package/src/Projection_GWT.res +312 -0
- package/src/Projection_GWT.res.mjs +332 -0
- package/src/Query_GWT.res +379 -0
- package/src/Query_GWT.res.mjs +367 -0
- package/src/RenderRescript.res +125 -0
- package/src/RenderRescript.res.mjs +141 -0
- package/src/RunnerTypes.res +68 -0
- package/src/RunnerTypes.res.mjs +60 -0
- package/src/SideEffect_GWT.res +157 -0
- package/src/SideEffect_GWT.res.mjs +113 -0
- package/src/StateChange/ExternalAddCategorySlice.res +25 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
- package/src/StateChange/WithFixtures.res +22 -0
- package/src/StateChange/WithFixtures.res.mjs +31 -0
- package/src/StateChange/WithFixtures_Behavior.res +17 -0
- package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
- package/src/StateChange/WithManualOpen.res +23 -0
- package/src/StateChange/WithManualOpen.res.mjs +31 -0
- package/src/StateChange/WithManualOpen_Behavior.res +17 -0
- package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
- package/src/StubRuntime.res +53 -0
- package/src/StubRuntime.res.mjs +54 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +92 -0
- package/src/Watch.res.mjs +83 -0
- package/src/WatcherProbe.res +48 -0
- package/src/WatcherProbe.res.mjs +46 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +94 -0
- package/tests/BuildClassifierTest.res.mjs +132 -0
- package/tests/ComponentMetaTest.res +52 -0
- package/tests/ComponentMetaTest.res.mjs +57 -0
- package/tests/DelegateGwtTest.res +487 -0
- package/tests/DelegateGwtTest.res.mjs +891 -0
- package/tests/DiscoveryTest.res +42 -0
- package/tests/DiscoveryTest.res.mjs +43 -0
- package/tests/DomainDeadCodeTest.res +116 -0
- package/tests/DomainDeadCodeTest.res.mjs +155 -0
- package/tests/DomainGraphTest.res +365 -0
- package/tests/DomainGraphTest.res.mjs +413 -0
- package/tests/FlowAggregateGwtTest.res +169 -0
- package/tests/FlowAggregateGwtTest.res.mjs +269 -0
- package/tests/FlowCrossPluginGwtTest.res +340 -0
- package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
- package/tests/FlowGwtTest.res +222 -0
- package/tests/FlowGwtTest.res.mjs +432 -0
- package/tests/FormatterVsCodeMessageTest.res +41 -0
- package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
- package/tests/InboundTranslationGwtTest.res +34 -0
- package/tests/InboundTranslationGwtTest.res.mjs +98 -0
- package/tests/LocalHostIntegration.res +70 -0
- package/tests/LocalHostIntegration.res.mjs +64 -0
- package/tests/LocalHostTest.res +71 -0
- package/tests/LocalHostTest.res.mjs +63 -0
- package/tests/MappingGwtTest.res +348 -0
- package/tests/MappingGwtTest.res.mjs +592 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
- package/tests/PlatformRunnerTest.res +66 -0
- package/tests/PlatformRunnerTest.res.mjs +85 -0
- package/tests/PlatformScanTest.res +61 -0
- package/tests/PlatformScanTest.res.mjs +56 -0
- package/tests/QueryGwtTest.res +161 -0
- package/tests/QueryGwtTest.res.mjs +314 -0
- package/tests/QueryResolverGwtTest.res +71 -0
- package/tests/QueryResolverGwtTest.res.mjs +136 -0
- package/tests/RunnerUnitTest.res +219 -0
- package/tests/RunnerUnitTest.res.mjs +244 -0
- package/tests/SideEffect/FakeOrderNotification.res +23 -0
- package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
- package/tests/SideEffect/MockEmail.res +18 -0
- package/tests/SideEffect/MockEmail.res.mjs +47 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
- package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
- package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithFixtures_GWT.res +23 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithManualOpen_GWT.res +17 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
- package/tests/StateChangeSliceGwtTest.res +160 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
- package/tests/StateViewSliceGwtTest.res +64 -0
- package/tests/StateViewSliceGwtTest.res.mjs +161 -0
- package/tests/WatchDebounceTest.res +78 -0
- 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
|
+
})
|