@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,269 @@
|
|
|
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
|
+
evolve: evolve,
|
|
62
|
+
decide: decide,
|
|
63
|
+
moduleUrl: moduleUrl
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let commandSchema$1 = S.union([
|
|
67
|
+
S.schema(s => ({
|
|
68
|
+
TAG: "Place",
|
|
69
|
+
customerId: s.m(S.string),
|
|
70
|
+
productIds: s.m(S.array(S.string))
|
|
71
|
+
})),
|
|
72
|
+
S.literal("Ship")
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
let eventSchema$1 = S.union([
|
|
76
|
+
S.schema(s => ({
|
|
77
|
+
TAG: "Placed",
|
|
78
|
+
customerId: s.m(S.string),
|
|
79
|
+
productIds: s.m(S.array(S.string))
|
|
80
|
+
})),
|
|
81
|
+
S.literal("Shipped")
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
let errorSchema$1 = S.union([
|
|
85
|
+
S.literal("AlreadyPlaced"),
|
|
86
|
+
S.literal("NotPlaced")
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
function commandAuthorization$1(param) {
|
|
90
|
+
return "AllowAuthenticated";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let OrderAggregate = {
|
|
94
|
+
name: "Order",
|
|
95
|
+
commandSchema: commandSchema$1,
|
|
96
|
+
eventSchema: eventSchema$1,
|
|
97
|
+
errorSchema: errorSchema$1,
|
|
98
|
+
commandAuthorization: commandAuthorization$1
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let initialState = {
|
|
102
|
+
placed: false,
|
|
103
|
+
shipped: false
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function evolve$1(state, event) {
|
|
107
|
+
if (typeof event !== "object") {
|
|
108
|
+
return {
|
|
109
|
+
placed: state.placed,
|
|
110
|
+
shipped: true
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
placed: true,
|
|
115
|
+
shipped: state.shipped
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function decide$1(state, command) {
|
|
121
|
+
if (typeof command !== "object") {
|
|
122
|
+
if (state.placed) {
|
|
123
|
+
if (state.shipped) {
|
|
124
|
+
return {
|
|
125
|
+
TAG: "Ok",
|
|
126
|
+
_0: []
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
return {
|
|
130
|
+
TAG: "Ok",
|
|
131
|
+
_0: ["Shipped"]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
return {
|
|
136
|
+
TAG: "Error",
|
|
137
|
+
_0: "NotPlaced"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
} else if (state.placed) {
|
|
141
|
+
return {
|
|
142
|
+
TAG: "Error",
|
|
143
|
+
_0: "AlreadyPlaced"
|
|
144
|
+
};
|
|
145
|
+
} else {
|
|
146
|
+
return {
|
|
147
|
+
TAG: "Ok",
|
|
148
|
+
_0: [{
|
|
149
|
+
TAG: "Placed",
|
|
150
|
+
customerId: command.customerId,
|
|
151
|
+
productIds: command.productIds
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let moduleUrl$1 = "test://OrderBehavior";
|
|
158
|
+
|
|
159
|
+
let OrderBehavior = {
|
|
160
|
+
Spec: undefined,
|
|
161
|
+
initialState: initialState,
|
|
162
|
+
evolve: evolve$1,
|
|
163
|
+
decide: decide$1,
|
|
164
|
+
moduleUrl: moduleUrl$1
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
let Sync = Flow_GWT$ReventlessGwt.AggregateCommandStep(CatalogProductAggregate)({
|
|
168
|
+
Spec: CatalogProductAggregate,
|
|
169
|
+
initialState: "NotSynced",
|
|
170
|
+
evolve: evolve,
|
|
171
|
+
decide: decide,
|
|
172
|
+
moduleUrl: moduleUrl
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
let Place = Flow_GWT$ReventlessGwt.AggregateCommandStep(OrderAggregate)({
|
|
176
|
+
Spec: OrderAggregate,
|
|
177
|
+
initialState: initialState,
|
|
178
|
+
evolve: evolve$1,
|
|
179
|
+
decide: decide$1,
|
|
180
|
+
moduleUrl: moduleUrl$1
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
Flow_GWT$ReventlessGwt.describe("Aggregate flow (single plugin)", () => {
|
|
184
|
+
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", {
|
|
185
|
+
TAG: "SyncNewProduct",
|
|
186
|
+
productId: "p1",
|
|
187
|
+
name: "Book"
|
|
188
|
+
}), {
|
|
189
|
+
TAG: "CatalogProductSynced",
|
|
190
|
+
productId: "p1",
|
|
191
|
+
name: "Book"
|
|
192
|
+
}), "o1", {
|
|
193
|
+
TAG: "Place",
|
|
194
|
+
customerId: "c1",
|
|
195
|
+
productIds: ["p1"]
|
|
196
|
+
}), {
|
|
197
|
+
TAG: "Placed",
|
|
198
|
+
customerId: "c1",
|
|
199
|
+
productIds: ["p1"]
|
|
200
|
+
}), "o1", "Ship"), "Shipped"));
|
|
201
|
+
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", {
|
|
202
|
+
TAG: "Place",
|
|
203
|
+
customerId: "c1",
|
|
204
|
+
productIds: ["p1"]
|
|
205
|
+
}), "o1", {
|
|
206
|
+
TAG: "Place",
|
|
207
|
+
customerId: "c1",
|
|
208
|
+
productIds: ["p1"]
|
|
209
|
+
}), "AlreadyPlaced"));
|
|
210
|
+
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", {
|
|
211
|
+
TAG: "Place",
|
|
212
|
+
customerId: "c1",
|
|
213
|
+
productIds: ["p1"]
|
|
214
|
+
}), {
|
|
215
|
+
TAG: "Placed",
|
|
216
|
+
customerId: "c1",
|
|
217
|
+
productIds: ["p1"]
|
|
218
|
+
}), "o2", {
|
|
219
|
+
TAG: "Place",
|
|
220
|
+
customerId: "c2",
|
|
221
|
+
productIds: ["p2"]
|
|
222
|
+
}), {
|
|
223
|
+
TAG: "Placed",
|
|
224
|
+
customerId: "c2",
|
|
225
|
+
productIds: ["p2"]
|
|
226
|
+
}));
|
|
227
|
+
Flow_GWT$ReventlessGwt.test("ship before place returns NotPlaced", undefined, () => Place.thenError(Place.whenCommand(Flow_GWT$ReventlessGwt.start, "o1", "Ship"), "NotPlaced"));
|
|
228
|
+
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", [{
|
|
229
|
+
TAG: "Placed",
|
|
230
|
+
customerId: "c1",
|
|
231
|
+
productIds: ["p1"]
|
|
232
|
+
}]), "o1", "Ship"), "Shipped"));
|
|
233
|
+
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", {
|
|
234
|
+
TAG: "Place",
|
|
235
|
+
customerId: "c1",
|
|
236
|
+
productIds: ["p1"]
|
|
237
|
+
}), "o1", "Ship"), "o1", "Ship")));
|
|
238
|
+
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", {
|
|
239
|
+
TAG: "SyncNewProduct",
|
|
240
|
+
productId: "p1",
|
|
241
|
+
name: "Book"
|
|
242
|
+
}), {
|
|
243
|
+
TAG: "CatalogProductSynced",
|
|
244
|
+
productId: "p1",
|
|
245
|
+
name: "Book"
|
|
246
|
+
}), "p2", {
|
|
247
|
+
TAG: "SyncNewProduct",
|
|
248
|
+
productId: "p2",
|
|
249
|
+
name: "Pen"
|
|
250
|
+
}), {
|
|
251
|
+
TAG: "CatalogProductSynced",
|
|
252
|
+
productId: "p2",
|
|
253
|
+
name: "Pen"
|
|
254
|
+
}), "p1", {
|
|
255
|
+
TAG: "SyncNewProduct",
|
|
256
|
+
productId: "p1",
|
|
257
|
+
name: "Book"
|
|
258
|
+
}), "AlreadySynced"));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export {
|
|
262
|
+
CatalogProductAggregate,
|
|
263
|
+
CatalogProductBehavior,
|
|
264
|
+
OrderAggregate,
|
|
265
|
+
OrderBehavior,
|
|
266
|
+
Sync,
|
|
267
|
+
Place,
|
|
268
|
+
}
|
|
269
|
+
/* 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
|
+
})
|