@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
package/src/Flow_GWT.res
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
open ReventlessCore
|
|
2
|
+
|
|
3
|
+
// Flow_GWT — cross-slice / end-to-end GWT.
|
|
4
|
+
//
|
|
5
|
+
// Threads ONE JSON-erased event log through a chain of slices, so a single
|
|
6
|
+
// declarative test verifies that the tiles of an Event Modeling board connect
|
|
7
|
+
// the way the diagram says: a command lands, an event is recorded, an
|
|
8
|
+
// automation reacts and issues the next command, a read model updates, an
|
|
9
|
+
// outbound effect fires. Where every other GWT kind verifies one tile in
|
|
10
|
+
// isolation, this one verifies the wiring between them.
|
|
11
|
+
//
|
|
12
|
+
// Slices are pure, so the flow runs with no bus, no scheduler and no timers:
|
|
13
|
+
// each step folds the slice's own `decide`/`evolve`/`collect`/`project` over
|
|
14
|
+
// the shared log. The log is JSON-erased (each step (de)serialises through its
|
|
15
|
+
// own spec's schema, exactly as the runtime routes a DCB event log), and each
|
|
16
|
+
// command step honours DCB tag filtering so downstream deciders never see
|
|
17
|
+
// phantom cross-entity events.
|
|
18
|
+
//
|
|
19
|
+
// The chain is built from per-step functors (one per slice), threaded with
|
|
20
|
+
// pipe-first so it reads top-to-bottom:
|
|
21
|
+
//
|
|
22
|
+
// module Place = Flow_GWT.CommandStep(PlaceOrder, PlaceOrder_Behavior)
|
|
23
|
+
// module Auto = Flow_GWT.AutomationStep(AutoShipOrderSlice)
|
|
24
|
+
// module Ship = Flow_GWT.CommandStep(ShipOrder, ShipOrder_Behavior)
|
|
25
|
+
// module View = Flow_GWT.ViewStep(Orders, Orders_Projection)
|
|
26
|
+
//
|
|
27
|
+
// start
|
|
28
|
+
// ->Sync.givenEvents([CatalogProductSynced({...})])
|
|
29
|
+
// ->Place.whenCommand(PlaceOrder({...}))->Place.thenEvents([OrderPlaced({...})])
|
|
30
|
+
// ->Auto.whenReacts->Auto.thenIssuesCommand(ShipOrder({...}))
|
|
31
|
+
// ->Ship.whenCommand(ShipOrder({...}))->Ship.thenEvents([OrderShipped({...})])
|
|
32
|
+
// ->View.thenViewState("o1", {...})
|
|
33
|
+
//
|
|
34
|
+
// Cross-plugin flows (Phase 3) add two boundary steps — `ExtensionPointStep`
|
|
35
|
+
// and `ExtensionStep` — that continue a flow from one plugin into the next
|
|
36
|
+
// through the ExtensionPoint mapping / Extension delegate, exactly as the
|
|
37
|
+
// runtime routes it across the plugin boundary.
|
|
38
|
+
//
|
|
39
|
+
// See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phases 2–3.
|
|
40
|
+
|
|
41
|
+
module EPMapping = ReventlessInfra.ExtensionPointMapping
|
|
42
|
+
module ExtMapping = ReventlessInfra.ExtensionMapping
|
|
43
|
+
|
|
44
|
+
// -- Shared log + flow state -------------------------------------------------
|
|
45
|
+
|
|
46
|
+
// One JSON-erased entry on the shared log: the variant name, the DCB tags
|
|
47
|
+
// extracted from the producing schema (so downstream tag filtering matches the
|
|
48
|
+
// runtime), the encoded event JSON, and — for events emitted by an aggregate
|
|
49
|
+
// step — the producing aggregate-ID. DCB step entries leave `aggregateId` as
|
|
50
|
+
// `None`; aggregate step entries set it to `Some(id)` so the matching
|
|
51
|
+
// `AggregateCommandStep` can rebuild per-aggregate history without DCB tag
|
|
52
|
+
// inference. Filtering by `aggregateId` and filtering by DCB tags are mutually
|
|
53
|
+
// exclusive: DCB queries look only at `eventType` + `tags`, and the aggregate
|
|
54
|
+
// decoder looks only at `aggregateId` + `eventType`.
|
|
55
|
+
type logEntry = {
|
|
56
|
+
eventType: string,
|
|
57
|
+
tags: array<Reventless.DcbTag.tag>,
|
|
58
|
+
json: JSON.t,
|
|
59
|
+
aggregateId: option<string>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The flow state threaded through every step. `outcome` accumulates assertions
|
|
63
|
+
// (first failure wins); the `last*` fields carry the most recent `when*`
|
|
64
|
+
// result for the matching `then*` to read.
|
|
65
|
+
type flowState = {
|
|
66
|
+
log: array<logEntry>,
|
|
67
|
+
outcome: Outcome.outcome,
|
|
68
|
+
lastEvents: array<JSON.t>, // encoded events from the last command step
|
|
69
|
+
lastError: option<JSON.t>, // encoded error from the last command step
|
|
70
|
+
lastCommands: array<JSON.t>, // encoded commands from the last automation / extension reaction
|
|
71
|
+
lastPublic: array<JSON.t>, // encoded public EP events from the last ExtensionPoint step
|
|
72
|
+
// The producing aggregate-ID for `lastEvents`, when the producer was an
|
|
73
|
+
// `AggregateCommandStep`. Aggregate-style EP mappings reference the source
|
|
74
|
+
// aggregate-ID inside `mapOutgoingEvent` (e.g. the catalog Product EP uses
|
|
75
|
+
// it as `productId`); the next `ExtensionPointStep` reads this field and
|
|
76
|
+
// threads it through. DCB `CommandStep` events leave it `None` and the EP
|
|
77
|
+
// step falls back to the synthetic `gwt-id` so the existing DCB Flow tests
|
|
78
|
+
// are unchanged.
|
|
79
|
+
lastAggregateId: option<string>,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type flow = promise<flowState>
|
|
83
|
+
|
|
84
|
+
S.enableJson()
|
|
85
|
+
|
|
86
|
+
let emptyState = {
|
|
87
|
+
log: [],
|
|
88
|
+
outcome: Outcome.pass,
|
|
89
|
+
lastEvents: [],
|
|
90
|
+
lastError: None,
|
|
91
|
+
lastCommands: [],
|
|
92
|
+
lastPublic: [],
|
|
93
|
+
lastAggregateId: None,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Entry point for a chain — an empty flow with no prior history.
|
|
97
|
+
let start: flow = Promise.resolve(emptyState)
|
|
98
|
+
|
|
99
|
+
// First failure wins: once the flow has recorded a mismatch, later assertions
|
|
100
|
+
// keep it rather than overwrite with a (possibly spurious) downstream result.
|
|
101
|
+
let recordOutcome = (s: flowState, o: Outcome.outcome): flowState =>
|
|
102
|
+
switch s.outcome {
|
|
103
|
+
| Error(_) => s
|
|
104
|
+
| Ok() => {...s, outcome: o}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Mirror the in-memory DCB event log's `matchesQuery`: an empty query matches
|
|
108
|
+
// everything; otherwise an entry matches if ANY clause matches (its event type
|
|
109
|
+
// is in the clause's `eventTypes` AND it carries every tag the clause lists).
|
|
110
|
+
let entryMatchesQuery = (entry: logEntry, query: Reventless.DcbTag.query) =>
|
|
111
|
+
if query->Array.length == 0 {
|
|
112
|
+
true
|
|
113
|
+
} else {
|
|
114
|
+
query->Array.some(qi => {
|
|
115
|
+
let typeMatch = switch qi.eventTypes {
|
|
116
|
+
| Some(types) => types->Array.includes(entry.eventType)
|
|
117
|
+
| None => true
|
|
118
|
+
}
|
|
119
|
+
let tagMatch = switch qi.tags {
|
|
120
|
+
| Some(tags) =>
|
|
121
|
+
tags->Array.every(tag =>
|
|
122
|
+
entry.tags->Array.some(et => et.key == tag.key && et.value == tag.value)
|
|
123
|
+
)
|
|
124
|
+
| None => true
|
|
125
|
+
}
|
|
126
|
+
typeMatch && tagMatch
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Encode + tag a batch of typed events and append them to the log.
|
|
131
|
+
let appendEvents = (log: array<logEntry>, events: array<'e>, schema: S.t<'e>): array<logEntry> =>
|
|
132
|
+
Array.concat(
|
|
133
|
+
log,
|
|
134
|
+
events->Array.map(ev => {
|
|
135
|
+
let json = ev->Message.encode(schema)
|
|
136
|
+
{
|
|
137
|
+
eventType: json->Reventless.Message.variantNameOfJson,
|
|
138
|
+
tags: Reventless.DcbTag.extractTagsExpanded(schema, ev),
|
|
139
|
+
json,
|
|
140
|
+
aggregateId: None,
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Aggregate-style append: encode + tag with the producing aggregate-ID so
|
|
146
|
+
// `decodeAggregateMatching` can rebuild that aggregate's history later. DCB
|
|
147
|
+
// tags are intentionally still extracted (cheap and lets a downstream DCB
|
|
148
|
+
// step pick the events up if any are tagged), but the `aggregateId` is the
|
|
149
|
+
// authoritative filter for aggregate consumers.
|
|
150
|
+
let appendAggregateEvents = (
|
|
151
|
+
log: array<logEntry>,
|
|
152
|
+
~id: string,
|
|
153
|
+
events: array<'e>,
|
|
154
|
+
schema: S.t<'e>,
|
|
155
|
+
): array<logEntry> =>
|
|
156
|
+
Array.concat(
|
|
157
|
+
log,
|
|
158
|
+
events->Array.map(ev => {
|
|
159
|
+
let json = ev->Message.encode(schema)
|
|
160
|
+
{
|
|
161
|
+
eventType: json->Reventless.Message.variantNameOfJson,
|
|
162
|
+
tags: Reventless.DcbTag.extractTagsExpanded(schema, ev),
|
|
163
|
+
json,
|
|
164
|
+
aggregateId: Some(id),
|
|
165
|
+
}
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Decode the log entries a consumer cares about. `query` filters by event type
|
|
170
|
+
// and DCB tags (use `[]` to take all of the consumer's event types); the
|
|
171
|
+
// decoder drops entries whose type the consumer does not declare.
|
|
172
|
+
let decodeMatching = (
|
|
173
|
+
log: array<logEntry>,
|
|
174
|
+
decoder: Reventless.DcbDecode.makeDecoderResult<'c>,
|
|
175
|
+
query: Reventless.DcbTag.query,
|
|
176
|
+
): array<'c> =>
|
|
177
|
+
log->Array.filterMap(entry =>
|
|
178
|
+
if entry->entryMatchesQuery(query) {
|
|
179
|
+
let data = switch entry.json {
|
|
180
|
+
| Object(d) => d
|
|
181
|
+
| _ => Dict.make()
|
|
182
|
+
}
|
|
183
|
+
decoder.decode(~eventType=entry.eventType, ~data)
|
|
184
|
+
} else {
|
|
185
|
+
None
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// Aggregate-style decode: filter the log to entries this aggregate-step
|
|
190
|
+
// emitted for `id`, then decode them with the aggregate's event schema. No
|
|
191
|
+
// DCB tag inference — partitioning is by `aggregateId` only, matching the
|
|
192
|
+
// way `Reventless.Aggregate` folds events at runtime.
|
|
193
|
+
let decodeAggregateMatching = (
|
|
194
|
+
log: array<logEntry>,
|
|
195
|
+
~id: string,
|
|
196
|
+
decoder: Reventless.DcbDecode.makeDecoderResult<'c>,
|
|
197
|
+
): array<'c> =>
|
|
198
|
+
log->Array.filterMap(entry =>
|
|
199
|
+
switch entry.aggregateId {
|
|
200
|
+
| Some(eid) if eid == id =>
|
|
201
|
+
let data = switch entry.json {
|
|
202
|
+
| Object(d) => d
|
|
203
|
+
| _ => Dict.make()
|
|
204
|
+
}
|
|
205
|
+
decoder.decode(~eventType=entry.eventType, ~data)
|
|
206
|
+
| _ => None
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
// -- Top-level test registration ---------------------------------------------
|
|
211
|
+
|
|
212
|
+
let describe = JestBind.describe
|
|
213
|
+
|
|
214
|
+
// The chain returns a `flow`; `test` awaits it and reports the accumulated
|
|
215
|
+
// outcome. Bodies read top-to-bottom and end on the last `then*`.
|
|
216
|
+
let test = (name, ~timeout=?, body: unit => flow) =>
|
|
217
|
+
JestBind.testPromise(~slice="Flow", name, ~timeout?, async () => {
|
|
218
|
+
let s = await body()
|
|
219
|
+
s.outcome
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// -- CommandStep: a StateChangeSlice / Aggregate decider ---------------------
|
|
223
|
+
|
|
224
|
+
module CommandStep = (
|
|
225
|
+
Spec: Behavior_GWT.BehaviorSpec,
|
|
226
|
+
Behavior: Behavior_GWT.Behavior with module Spec := Spec,
|
|
227
|
+
) => {
|
|
228
|
+
let consumedDecoder = Reventless.DcbDecode.makeDecoder(Spec.consumedEventSchema)
|
|
229
|
+
let consumedEventTypes = consumedDecoder.eventTypes
|
|
230
|
+
|
|
231
|
+
// Seed prior history with events this slice can emit.
|
|
232
|
+
let givenEvents = async (flowP: flow, events: array<Spec.event>) => {
|
|
233
|
+
let s = await flowP
|
|
234
|
+
{...s, log: s.log->appendEvents(events, Spec.eventSchema)}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Filter the log by the command's DCB tags, fold `evolve`, run `decide`, and
|
|
238
|
+
// append the emitted events back onto the shared log.
|
|
239
|
+
// Scope derived from this slice's schemas, mirroring the StateChangeSlice runtime:
|
|
240
|
+
// cross-partition keys (explicit @crossPartition ∪ per-slice inference) fan a
|
|
241
|
+
// foreign reference into its own clause, and the per-event indexed-tag map narrows
|
|
242
|
+
// a foreign reference on this slice's own emitted event out of that clause (so the
|
|
243
|
+
// real tagged log here does not return sibling entities).
|
|
244
|
+
let scopeShape = Reventless.DcbTag.sliceShapeFromSchemas(
|
|
245
|
+
~name="",
|
|
246
|
+
~commandSchema=Spec.commandSchema,
|
|
247
|
+
~consumedEventSchema=Spec.consumedEventSchema,
|
|
248
|
+
~eventSchema=Spec.eventSchema,
|
|
249
|
+
)
|
|
250
|
+
let crossPartitionTagKeys = {
|
|
251
|
+
let set = Set.make()
|
|
252
|
+
Reventless.DcbTag.extractCrossPartitionTagKeys(Spec.eventSchema)->Array.forEach(k =>
|
|
253
|
+
set->Set.add(k)
|
|
254
|
+
)
|
|
255
|
+
Reventless.DcbScopeInference.crossPartitionForSlice(scopeShape)->Array.forEach(k =>
|
|
256
|
+
set->Set.add(k)
|
|
257
|
+
)
|
|
258
|
+
Array.fromIterator(set->Set.values)->Array.toSorted((a, b) => String.compare(a, b))
|
|
259
|
+
}
|
|
260
|
+
let tagKeysByEventType =
|
|
261
|
+
Reventless.DcbScopeInference.infer([scopeShape]).tagKeysByEventType
|
|
262
|
+
|
|
263
|
+
let whenCommand = async (flowP: flow, command: Spec.command) => {
|
|
264
|
+
let s = await flowP
|
|
265
|
+
let query = Reventless.DcbTag.buildQueryFromCommand(
|
|
266
|
+
~eventTypes=consumedEventTypes,
|
|
267
|
+
~schema=Spec.commandSchema,
|
|
268
|
+
~value=command,
|
|
269
|
+
~tagKeysByEventType,
|
|
270
|
+
~crossPartitionTagKeys,
|
|
271
|
+
)
|
|
272
|
+
let history = decodeMatching(s.log, consumedDecoder, query)
|
|
273
|
+
let state = history->Array.reduce(Behavior.initialState, Behavior.evolve)
|
|
274
|
+
switch Behavior.decide(state, command) {
|
|
275
|
+
| Ok(events) => {
|
|
276
|
+
...s,
|
|
277
|
+
log: s.log->appendEvents(events, Spec.eventSchema),
|
|
278
|
+
lastEvents: events->Array.map(e => e->Message.encode(Spec.eventSchema)),
|
|
279
|
+
lastError: None,
|
|
280
|
+
lastAggregateId: None,
|
|
281
|
+
}
|
|
282
|
+
| Error(error) => {
|
|
283
|
+
...s,
|
|
284
|
+
lastEvents: [],
|
|
285
|
+
lastError: Some(error->Message.encode(Spec.errorSchema)),
|
|
286
|
+
lastAggregateId: None,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let thenEvents = async (flowP: flow, expected: array<Spec.event>) => {
|
|
292
|
+
let s = await flowP
|
|
293
|
+
let expectedJson = expected->Array.map(e => e->Message.encode(Spec.eventSchema))
|
|
294
|
+
let o = switch s.lastError {
|
|
295
|
+
| Some(err) =>
|
|
296
|
+
Outcome.fail(
|
|
297
|
+
ErrorMismatch({expected: JSON.Encode.null, actual: Some(err), actualEvents: s.lastEvents}),
|
|
298
|
+
)
|
|
299
|
+
| None =>
|
|
300
|
+
s.lastEvents == expectedJson
|
|
301
|
+
? Outcome.pass
|
|
302
|
+
: Outcome.fail(EventsMismatch({expected: expectedJson, actual: s.lastEvents}))
|
|
303
|
+
}
|
|
304
|
+
s->recordOutcome(o)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let thenEvent = (flowP, event) => thenEvents(flowP, [event])
|
|
308
|
+
|
|
309
|
+
let thenError = async (flowP: flow, expected: Spec.error) => {
|
|
310
|
+
let s = await flowP
|
|
311
|
+
let expJson = expected->Message.encode(Spec.errorSchema)
|
|
312
|
+
let o = switch s.lastError {
|
|
313
|
+
| None =>
|
|
314
|
+
Outcome.fail(ErrorMismatch({expected: expJson, actual: None, actualEvents: s.lastEvents}))
|
|
315
|
+
| Some(actual) =>
|
|
316
|
+
actual == expJson
|
|
317
|
+
? Outcome.pass
|
|
318
|
+
: Outcome.fail(
|
|
319
|
+
ErrorMismatch({expected: expJson, actual: Some(actual), actualEvents: s.lastEvents}),
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
s->recordOutcome(o)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// -- AggregateCommandStep: an Aggregate decider partitioned by aggregate-ID --
|
|
327
|
+
//
|
|
328
|
+
// Mirror of `CommandStep` for `Reventless.Behavior.T` (the aggregate-flavor
|
|
329
|
+
// behavior). Differences from the slice form:
|
|
330
|
+
// * No `consumedEvent` distinction — aggregates fold their own `event`
|
|
331
|
+
// stream; the decoder uses `Spec.eventSchema`.
|
|
332
|
+
// * No DCB tag inference — `~id` partitions the history. `givenEvents` and
|
|
333
|
+
// `whenCommand` both take `~id`, and the emitted events are stamped with
|
|
334
|
+
// the same `id` on the shared log so a subsequent step can read them
|
|
335
|
+
// back.
|
|
336
|
+
//
|
|
337
|
+
// Sequencing two aggregate steps for the same `~id` rebuilds the running
|
|
338
|
+
// state across the chain; sequencing them for different `~id` keeps their
|
|
339
|
+
// histories isolated (a `Place` on `o2` does not see the events of `o1`).
|
|
340
|
+
|
|
341
|
+
module AggregateCommandStep = (
|
|
342
|
+
Spec: Behavior_GWT.AggregateSpec,
|
|
343
|
+
Behavior: Reventless.Behavior.T with module Spec = Spec,
|
|
344
|
+
) => {
|
|
345
|
+
let aggregateDecoder = Reventless.DcbDecode.makeDecoder(Spec.eventSchema)
|
|
346
|
+
|
|
347
|
+
// Seed `id`'s history before the first `whenCommand`. Useful when a flow
|
|
348
|
+
// starts mid-stream (e.g. the order is already Placed when the test begins).
|
|
349
|
+
let givenEvents = async (flowP: flow, ~id: string, events: array<Spec.event>) => {
|
|
350
|
+
let s = await flowP
|
|
351
|
+
{...s, log: s.log->appendAggregateEvents(~id, events, Spec.eventSchema)}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Rebuild `id`'s state from the log, run `decide`, then either append the
|
|
355
|
+
// emitted events (still tagged with `~id`) or remember the error for the
|
|
356
|
+
// next `thenError`.
|
|
357
|
+
let whenCommand = async (flowP: flow, ~id: string, command: Spec.command) => {
|
|
358
|
+
let s = await flowP
|
|
359
|
+
let history = decodeAggregateMatching(s.log, ~id, aggregateDecoder)
|
|
360
|
+
let state = history->Array.reduce(Behavior.initialState, Behavior.evolve)
|
|
361
|
+
switch Behavior.decide(state, command) {
|
|
362
|
+
| Ok(events) => {
|
|
363
|
+
...s,
|
|
364
|
+
log: s.log->appendAggregateEvents(~id, events, Spec.eventSchema),
|
|
365
|
+
lastEvents: events->Array.map(e => e->Message.encode(Spec.eventSchema)),
|
|
366
|
+
lastError: None,
|
|
367
|
+
lastAggregateId: Some(id),
|
|
368
|
+
}
|
|
369
|
+
| Error(error) => {
|
|
370
|
+
...s,
|
|
371
|
+
lastEvents: [],
|
|
372
|
+
lastError: Some(error->Message.encode(Spec.errorSchema)),
|
|
373
|
+
lastAggregateId: Some(id),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let thenEvents = async (flowP: flow, expected: array<Spec.event>) => {
|
|
379
|
+
let s = await flowP
|
|
380
|
+
let expectedJson = expected->Array.map(e => e->Message.encode(Spec.eventSchema))
|
|
381
|
+
let o = switch s.lastError {
|
|
382
|
+
| Some(err) =>
|
|
383
|
+
Outcome.fail(
|
|
384
|
+
ErrorMismatch({expected: JSON.Encode.null, actual: Some(err), actualEvents: s.lastEvents}),
|
|
385
|
+
)
|
|
386
|
+
| None =>
|
|
387
|
+
s.lastEvents == expectedJson
|
|
388
|
+
? Outcome.pass
|
|
389
|
+
: Outcome.fail(EventsMismatch({expected: expectedJson, actual: s.lastEvents}))
|
|
390
|
+
}
|
|
391
|
+
s->recordOutcome(o)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let thenEvent = (flowP, event) => thenEvents(flowP, [event])
|
|
395
|
+
|
|
396
|
+
let thenNoEvent = async (flowP: flow) => {
|
|
397
|
+
let s = await flowP
|
|
398
|
+
let o = switch s.lastError {
|
|
399
|
+
| Some(err) =>
|
|
400
|
+
Outcome.fail(
|
|
401
|
+
ErrorMismatch({expected: JSON.Encode.null, actual: Some(err), actualEvents: s.lastEvents}),
|
|
402
|
+
)
|
|
403
|
+
| None =>
|
|
404
|
+
s.lastEvents->Array.length == 0
|
|
405
|
+
? Outcome.pass
|
|
406
|
+
: Outcome.fail(NoEventExpected({actual: s.lastEvents}))
|
|
407
|
+
}
|
|
408
|
+
s->recordOutcome(o)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let thenError = async (flowP: flow, expected: Spec.error) => {
|
|
412
|
+
let s = await flowP
|
|
413
|
+
let expJson = expected->Message.encode(Spec.errorSchema)
|
|
414
|
+
let o = switch s.lastError {
|
|
415
|
+
| None =>
|
|
416
|
+
Outcome.fail(ErrorMismatch({expected: expJson, actual: None, actualEvents: s.lastEvents}))
|
|
417
|
+
| Some(actual) =>
|
|
418
|
+
actual == expJson
|
|
419
|
+
? Outcome.pass
|
|
420
|
+
: Outcome.fail(
|
|
421
|
+
ErrorMismatch({expected: expJson, actual: Some(actual), actualEvents: s.lastEvents}),
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
s->recordOutcome(o)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// -- AutomationStep: a policy that reacts to log events and issues a command --
|
|
429
|
+
|
|
430
|
+
module AutomationStep = (Spec: Automation_GWT.SliceSpec) => {
|
|
431
|
+
let consumedDecoder = Reventless.DcbDecode.makeDecoder(Spec.consumedEventSchema)
|
|
432
|
+
|
|
433
|
+
// Sweep the log: collect todos, drop the ones resolved within the same
|
|
434
|
+
// stream, process the remainder into commands.
|
|
435
|
+
let whenReacts = async (flowP: flow) => {
|
|
436
|
+
let s = await flowP
|
|
437
|
+
let events = decodeMatching(s.log, consumedDecoder, [])
|
|
438
|
+
let collected = events->Array.map(e => e->Spec.collect)->Array.flat
|
|
439
|
+
let resolvedIds = events->Array.filterMap(e => e->Spec.resolve)
|
|
440
|
+
let pending = collected->Array.filter(((id, _)) => !(resolvedIds->Array.includes(id)))
|
|
441
|
+
let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo))
|
|
442
|
+
{...s, lastCommands: commands->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema))}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let thenIssuesCommands = async (flowP: flow, expected: array<Spec.command>) => {
|
|
446
|
+
let s = await flowP
|
|
447
|
+
let expJson = expected->Array.map(c => c->Message.encode(Spec.commandSchema))
|
|
448
|
+
let o =
|
|
449
|
+
s.lastCommands == expJson
|
|
450
|
+
? Outcome.pass
|
|
451
|
+
: Outcome.fail(EventsMismatch({expected: expJson, actual: s.lastCommands}))
|
|
452
|
+
s->recordOutcome(o)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let thenIssuesCommand = (flowP, command) => thenIssuesCommands(flowP, [command])
|
|
456
|
+
let thenIssuesNoCommand = flowP => thenIssuesCommands(flowP, [])
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// -- ViewStep: a StateViewSlice / projection over the log --------------------
|
|
460
|
+
|
|
461
|
+
module ViewStep = (
|
|
462
|
+
Spec: Projection_GWT.Spec,
|
|
463
|
+
Projection: Projection_GWT.Projection with module Spec := Spec,
|
|
464
|
+
) => {
|
|
465
|
+
module P = Projection_GWT.Make(Spec, Projection)
|
|
466
|
+
let consumedDecoder = Reventless.DcbDecode.makeDecoder(Spec.consumedEventSchema)
|
|
467
|
+
|
|
468
|
+
let encState = (st: Spec.state) => st->Message.encode(Spec.stateSchema)
|
|
469
|
+
|
|
470
|
+
// Fold the projection over every consumed event in the log and assert the
|
|
471
|
+
// row(s) for `id`.
|
|
472
|
+
let thenViewStates = async (flowP: flow, id, expected: array<Spec.state>) => {
|
|
473
|
+
let s = await flowP
|
|
474
|
+
let events = decodeMatching(s.log, consumedDecoder, [])
|
|
475
|
+
let store = await P.givenEvents(events)
|
|
476
|
+
let actual = store->Dict.get(id)->Option.getOr([])->Array.map(encState)
|
|
477
|
+
let expectedJson = expected->Array.map(encState)
|
|
478
|
+
let o =
|
|
479
|
+
actual == expectedJson
|
|
480
|
+
? Outcome.pass
|
|
481
|
+
: Outcome.fail(
|
|
482
|
+
StateMismatch({
|
|
483
|
+
key: id,
|
|
484
|
+
expected: Some(JSON.Encode.array(expectedJson)),
|
|
485
|
+
actual: Some(JSON.Encode.array(actual)),
|
|
486
|
+
}),
|
|
487
|
+
)
|
|
488
|
+
s->recordOutcome(o)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let thenViewState = (flowP, id, state) => thenViewStates(flowP, id, [state])
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// -- OutboundStep: an OutboundTranslationSlice's `collect` over the log -------
|
|
495
|
+
|
|
496
|
+
module OutboundStep = (Spec: OutboundTranslation_GWT.SliceSpec) => {
|
|
497
|
+
let consumedDecoder = Reventless.DcbDecode.makeDecoder(Spec.consumedEventSchema)
|
|
498
|
+
|
|
499
|
+
let encItems = (arr: array<(string, Spec.outboundItem)>) =>
|
|
500
|
+
arr->Array.map(((id, item)) => (id, item->Message.encode(Spec.outboundItemSchema)))
|
|
501
|
+
|
|
502
|
+
// Assert the outbound effects the slice would fire — the `collect` result.
|
|
503
|
+
// (The async `translate` leg hits an external service, so it is exercised
|
|
504
|
+
// only by the dedicated `OutboundTranslation_GWT`, not the flow.)
|
|
505
|
+
let thenOutbound = async (flowP: flow, expected: array<(string, Spec.outboundItem)>) => {
|
|
506
|
+
let s = await flowP
|
|
507
|
+
let events = decodeMatching(s.log, consumedDecoder, [])
|
|
508
|
+
let collected = events->Array.map(e => e->Spec.collect)->Array.flat
|
|
509
|
+
let actual = encItems(collected)
|
|
510
|
+
let expectedJson = encItems(expected)
|
|
511
|
+
let o =
|
|
512
|
+
actual == expectedJson
|
|
513
|
+
? Outcome.pass
|
|
514
|
+
: Outcome.fail(TodoMismatch({expected: expectedJson, actual}))
|
|
515
|
+
s->recordOutcome(o)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let thenOutboundNothing = flowP => thenOutbound(flowP, [])
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// -- Cross-plugin boundary steps (Phase 3) -----------------------------------
|
|
522
|
+
|
|
523
|
+
// Order-independent comparison for published actions — fan-out order is
|
|
524
|
+
// non-deterministic, so compare sorted by stringified JSON.
|
|
525
|
+
let sortJson = (arr: array<JSON.t>) =>
|
|
526
|
+
arr->Array.toSorted((a, b) => String.compare(JSON.stringify(a), JSON.stringify(b)))
|
|
527
|
+
|
|
528
|
+
// ExtensionPointStep: runs an EP mapping's `mapOutgoingEvent` over the events
|
|
529
|
+
// the last command step produced, turning internal events into the public
|
|
530
|
+
// extension-point events the next plugin subscribes to (handles one-to-many
|
|
531
|
+
// fan-out). The public events are appended to the shared log and remembered
|
|
532
|
+
// for `thenPublicEvent(s)` / the following `ExtensionStep`.
|
|
533
|
+
module ExtensionPointStep = (M: EPMapping.Mapping) => {
|
|
534
|
+
let delegateDecoder = Reventless.DcbDecode.makeDecoder(M.Delegate.eventSchema)
|
|
535
|
+
|
|
536
|
+
// Aggregate-style EP mappings address the source aggregate's identity via
|
|
537
|
+
// the first arg (e.g. catalog Product → ProductBecameAvailable uses it as
|
|
538
|
+
// `productId`). DCB upstreams don't set `lastAggregateId` and fall back to
|
|
539
|
+
// the synthetic `gwt-id` so existing DCB Flow tests keep their behaviour.
|
|
540
|
+
let runMapping = async (
|
|
541
|
+
sourceId: string,
|
|
542
|
+
events: array<M.Delegate.event>,
|
|
543
|
+
): array<M.ExtensionPoint.event> =>
|
|
544
|
+
switch M.mapOutgoingEvent {
|
|
545
|
+
| None => []
|
|
546
|
+
| Some(f) =>
|
|
547
|
+
let actions =
|
|
548
|
+
events
|
|
549
|
+
->Array.map(ev => f(sourceId, ev, StubRuntime.meta, StubRuntime.queryEngine))
|
|
550
|
+
->Array.flat
|
|
551
|
+
let nested =
|
|
552
|
+
await actions
|
|
553
|
+
->Array.map(async action =>
|
|
554
|
+
switch action {
|
|
555
|
+
| EPMapping.PublishEvent(_id, e) => [e]
|
|
556
|
+
| EPMapping.PublishEventAsync(p) =>
|
|
557
|
+
let (_id, e) = await p
|
|
558
|
+
[e]
|
|
559
|
+
| EPMapping.HandleDirective(_, _) => []
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
->Promise.all
|
|
563
|
+
nested->Array.flat
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let whenPublishedThrough = async (flowP: flow) => {
|
|
567
|
+
let s = await flowP
|
|
568
|
+
let delegateEvents =
|
|
569
|
+
s.lastEvents->Array.filterMap(json => {
|
|
570
|
+
let data = switch json {
|
|
571
|
+
| Object(d) => d
|
|
572
|
+
| _ => Dict.make()
|
|
573
|
+
}
|
|
574
|
+
delegateDecoder.decode(~eventType=json->Reventless.Message.variantNameOfJson, ~data)
|
|
575
|
+
})
|
|
576
|
+
let sourceId = s.lastAggregateId->Option.getOr("gwt-id")
|
|
577
|
+
let publicEvents = await runMapping(sourceId, delegateEvents)
|
|
578
|
+
{
|
|
579
|
+
...s,
|
|
580
|
+
log: s.log->appendEvents(publicEvents, M.ExtensionPoint.eventSchema),
|
|
581
|
+
lastPublic: publicEvents->Array.map(e => e->Message.encode(M.ExtensionPoint.eventSchema)),
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let thenPublicEvents = async (flowP: flow, expected: array<M.ExtensionPoint.event>) => {
|
|
586
|
+
let s = await flowP
|
|
587
|
+
let expJson = expected->Array.map(e => e->Message.encode(M.ExtensionPoint.eventSchema))
|
|
588
|
+
let o =
|
|
589
|
+
sortJson(s.lastPublic) == sortJson(expJson)
|
|
590
|
+
? Outcome.pass
|
|
591
|
+
: Outcome.fail(PublishedActionsMismatch({expected: expJson, actual: s.lastPublic}))
|
|
592
|
+
s->recordOutcome(o)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let thenPublicEvent = (flowP, event) => thenPublicEvents(flowP, [event])
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ExtensionStep: runs an Extension delegate's `mapIncomingEvent` over the public
|
|
599
|
+
// EP events the last ExtensionPoint step produced, surfacing the commands the
|
|
600
|
+
// extension issues onto its own plugin's slices. The next `CommandStep` then
|
|
601
|
+
// executes the asserted command.
|
|
602
|
+
module ExtensionStep = (M: ExtMapping.Mapping) => {
|
|
603
|
+
let epDecoder = Reventless.DcbDecode.makeDecoder(M.ExtensionPoint.eventSchema)
|
|
604
|
+
let encCmd = (c: M.Delegate.command) => c->Message.encode(M.Delegate.commandSchema)
|
|
605
|
+
|
|
606
|
+
let whenExtensionReacts = async (flowP: flow) => {
|
|
607
|
+
let s = await flowP
|
|
608
|
+
let epEvents =
|
|
609
|
+
s.lastPublic->Array.filterMap(json => {
|
|
610
|
+
let data = switch json {
|
|
611
|
+
| Object(d) => d
|
|
612
|
+
| _ => Dict.make()
|
|
613
|
+
}
|
|
614
|
+
epDecoder.decode(~eventType=json->Reventless.Message.variantNameOfJson, ~data)
|
|
615
|
+
})
|
|
616
|
+
let actions =
|
|
617
|
+
epEvents
|
|
618
|
+
->Array.map(ev =>
|
|
619
|
+
M.mapIncomingEvent(
|
|
620
|
+
"gwt-id",
|
|
621
|
+
ev,
|
|
622
|
+
StubRuntime.meta,
|
|
623
|
+
StubRuntime.pluginDefinition,
|
|
624
|
+
StubRuntime.queryEngine,
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
->Array.flat
|
|
628
|
+
let nested =
|
|
629
|
+
await actions
|
|
630
|
+
->Array.map(async action =>
|
|
631
|
+
switch action {
|
|
632
|
+
| ExtMapping.PublishStateChangeSliceCommand(cmd) => [encCmd(cmd)]
|
|
633
|
+
| ExtMapping.PublishStateChangeSliceCommandAsync(p) =>
|
|
634
|
+
let cmd = await p
|
|
635
|
+
[encCmd(cmd)]
|
|
636
|
+
| ExtMapping.PublishStateChangeSliceCommandsAsync(p) => (await p)->Array.map(encCmd)
|
|
637
|
+
| ExtMapping.PublishAggregateCommand(_id, cmd) => [encCmd(cmd)]
|
|
638
|
+
| ExtMapping.PublishAggregateCommandAsync(p) =>
|
|
639
|
+
let (_id, cmd) = await p
|
|
640
|
+
[encCmd(cmd)]
|
|
641
|
+
| ExtMapping.PublishAggregateCommandsAsync(p) =>
|
|
642
|
+
(await p)->Array.map(((_id, cmd)) => encCmd(cmd))
|
|
643
|
+
| ExtMapping.PublishExtensionPointCommand(_, _)
|
|
644
|
+
| ExtMapping.ForwardCommand(_)
|
|
645
|
+
| ExtMapping.HandleDirective(_, _) => []
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
->Promise.all
|
|
649
|
+
{...s, lastCommands: nested->Array.flat}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let thenIssuesCommands = async (flowP: flow, expected: array<M.Delegate.command>) => {
|
|
653
|
+
let s = await flowP
|
|
654
|
+
let expJson = expected->Array.map(encCmd)
|
|
655
|
+
let o =
|
|
656
|
+
sortJson(s.lastCommands) == sortJson(expJson)
|
|
657
|
+
? Outcome.pass
|
|
658
|
+
: Outcome.fail(PublishedActionsMismatch({expected: expJson, actual: s.lastCommands}))
|
|
659
|
+
s->recordOutcome(o)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let thenIssuesCommand = (flowP, command) => thenIssuesCommands(flowP, [command])
|
|
663
|
+
let thenIssuesNoCommand = flowP => thenIssuesCommands(flowP, [])
|
|
664
|
+
}
|