@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.
Files changed (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +603 -0
  10. package/src/Behavior_GWT.res.mjs +426 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +137 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +31 -0
  16. package/src/Cancellation.res.mjs +45 -0
  17. package/src/ChildProcess.res +84 -0
  18. package/src/ChildProcess.res.mjs +92 -0
  19. package/src/Cli.res +671 -0
  20. package/src/Cli.res.mjs +754 -0
  21. package/src/Collector.res +201 -0
  22. package/src/Collector.res.mjs +215 -0
  23. package/src/ComponentMeta.res +104 -0
  24. package/src/ComponentMeta.res.mjs +120 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +103 -0
  33. package/src/DomainDeadCode.res +93 -0
  34. package/src/DomainDeadCode.res.mjs +69 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +36 -0
  40. package/src/Filter.res.mjs +40 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +138 -0
  44. package/src/FormatterHuman.res.mjs +122 -0
  45. package/src/FormatterJson.res +257 -0
  46. package/src/FormatterJson.res.mjs +320 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +139 -0
  50. package/src/FormatterTap.res.mjs +149 -0
  51. package/src/FormatterVsCode.res +362 -0
  52. package/src/FormatterVsCode.res.mjs +564 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +135 -0
  62. package/src/LocalHost.res.mjs +127 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MultiSourceProjection_GWT.res +316 -0
  66. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  67. package/src/OutboundTranslation_GWT.res +238 -0
  68. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  69. package/src/Outcome.res +161 -0
  70. package/src/Outcome.res.mjs +140 -0
  71. package/src/PackageScan.res +80 -0
  72. package/src/PackageScan.res.mjs +81 -0
  73. package/src/PlatformRunner.res +160 -0
  74. package/src/PlatformRunner.res.mjs +155 -0
  75. package/src/PlatformScan.res +138 -0
  76. package/src/PlatformScan.res.mjs +131 -0
  77. package/src/ProcessManager.res +91 -0
  78. package/src/ProcessManager.res.mjs +80 -0
  79. package/src/Projection_GWT.res +312 -0
  80. package/src/Projection_GWT.res.mjs +332 -0
  81. package/src/Query_GWT.res +379 -0
  82. package/src/Query_GWT.res.mjs +367 -0
  83. package/src/RenderRescript.res +125 -0
  84. package/src/RenderRescript.res.mjs +141 -0
  85. package/src/RunnerTypes.res +68 -0
  86. package/src/RunnerTypes.res.mjs +60 -0
  87. package/src/SideEffect_GWT.res +157 -0
  88. package/src/SideEffect_GWT.res.mjs +113 -0
  89. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  90. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  91. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  92. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  93. package/src/StateChange/WithFixtures.res +22 -0
  94. package/src/StateChange/WithFixtures.res.mjs +31 -0
  95. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  96. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  97. package/src/StateChange/WithManualOpen.res +23 -0
  98. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  99. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  100. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  101. package/src/StubRuntime.res +53 -0
  102. package/src/StubRuntime.res.mjs +54 -0
  103. package/src/TestFixtures.res +16 -0
  104. package/src/TestFixtures.res.mjs +34 -0
  105. package/src/Watch.res +92 -0
  106. package/src/Watch.res.mjs +83 -0
  107. package/src/WatcherProbe.res +48 -0
  108. package/src/WatcherProbe.res.mjs +46 -0
  109. package/tests/AutomationGwtTest.res +71 -0
  110. package/tests/AutomationGwtTest.res.mjs +189 -0
  111. package/tests/BuildClassifierTest.res +94 -0
  112. package/tests/BuildClassifierTest.res.mjs +132 -0
  113. package/tests/ComponentMetaTest.res +52 -0
  114. package/tests/ComponentMetaTest.res.mjs +57 -0
  115. package/tests/DelegateGwtTest.res +487 -0
  116. package/tests/DelegateGwtTest.res.mjs +891 -0
  117. package/tests/DiscoveryTest.res +42 -0
  118. package/tests/DiscoveryTest.res.mjs +43 -0
  119. package/tests/DomainDeadCodeTest.res +116 -0
  120. package/tests/DomainDeadCodeTest.res.mjs +155 -0
  121. package/tests/DomainGraphTest.res +365 -0
  122. package/tests/DomainGraphTest.res.mjs +413 -0
  123. package/tests/FlowAggregateGwtTest.res +169 -0
  124. package/tests/FlowAggregateGwtTest.res.mjs +269 -0
  125. package/tests/FlowCrossPluginGwtTest.res +340 -0
  126. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  127. package/tests/FlowGwtTest.res +222 -0
  128. package/tests/FlowGwtTest.res.mjs +432 -0
  129. package/tests/FormatterVsCodeMessageTest.res +41 -0
  130. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  131. package/tests/InboundTranslationGwtTest.res +34 -0
  132. package/tests/InboundTranslationGwtTest.res.mjs +98 -0
  133. package/tests/LocalHostIntegration.res +70 -0
  134. package/tests/LocalHostIntegration.res.mjs +64 -0
  135. package/tests/LocalHostTest.res +71 -0
  136. package/tests/LocalHostTest.res.mjs +63 -0
  137. package/tests/MappingGwtTest.res +348 -0
  138. package/tests/MappingGwtTest.res.mjs +592 -0
  139. package/tests/OutboundTranslationGwtTest.res +59 -0
  140. package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
  141. package/tests/PlatformRunnerTest.res +66 -0
  142. package/tests/PlatformRunnerTest.res.mjs +85 -0
  143. package/tests/PlatformScanTest.res +61 -0
  144. package/tests/PlatformScanTest.res.mjs +56 -0
  145. package/tests/QueryGwtTest.res +161 -0
  146. package/tests/QueryGwtTest.res.mjs +314 -0
  147. package/tests/QueryResolverGwtTest.res +71 -0
  148. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  149. package/tests/RunnerUnitTest.res +219 -0
  150. package/tests/RunnerUnitTest.res.mjs +244 -0
  151. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  152. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  153. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  154. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  155. package/tests/SideEffect/MockEmail.res +18 -0
  156. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  157. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  158. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  159. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  160. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  161. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  162. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  163. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  164. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  165. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  166. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  167. package/tests/StateChangeSliceGwtTest.res +160 -0
  168. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  169. package/tests/StateViewSliceGwtTest.res +64 -0
  170. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  171. package/tests/WatchDebounceTest.res +78 -0
  172. package/tests/WatchDebounceTest.res.mjs +97 -0
@@ -0,0 +1,103 @@
1
+ // Walks each owning package's src/ tree and enumerates its Reventless
2
+ // components by folder convention — including components with no GWT tests.
3
+ // This is the inventory the `components` NDJSON event carries: the basis for
4
+ // the activity-bar Plugin→kind→component tree and "untested slice" coverage.
5
+ // Computed fresh per discovery so it never drifts as components are added.
6
+
7
+ type component = {
8
+ // Absolute directory of the owning package (matches the `dir` in `packages`).
9
+ dir: string,
10
+ kind: string,
11
+ name: string,
12
+ // Absolute paths of the component's `src/` files (spec + body files like
13
+ // `*_Behavior.res` / `*_Mappings.res`), in walk order. Lets the client list
14
+ // spec / implementation files under each component node.
15
+ files: array<string>,
16
+ }
17
+
18
+ type dirent = {
19
+ name: string,
20
+ @as("isDirectory") _isDirectory: unit => bool,
21
+ @as("isFile") _isFile: unit => bool,
22
+ }
23
+ type readdirOpts = {withFileTypes: bool}
24
+ @module("node:fs/promises")
25
+ external _readdir: (string, readdirOpts) => promise<array<dirent>> = "readdir"
26
+ @module("node:fs") external _existsSync: string => bool = "existsSync"
27
+ @module("node:path") external join: (string, string) => string = "join"
28
+
29
+ let ignoreNames = ["node_modules", ".git", "dist", "lib", ".history"]
30
+ let shouldIgnore = (name: string) => Array.includes(ignoreNames, name)
31
+
32
+ // A directory carrying this sentinel file — and its whole subtree — is pruned
33
+ // from the component scan, matching `Discovery`'s `.gwtignore` convention so
34
+ // the domain tree and the test tree exclude the same generated/vendored trees.
35
+ let gwtIgnoreFile = ".gwtignore"
36
+ let isPruned = (entries: array<dirent>) => entries->Array.some(e => e.name == gwtIgnoreFile)
37
+
38
+ // Source `.res` only — `.res.mjs` / `.res.js` end in `.mjs` / `.js`, and `.resi`
39
+ // in `.resi`, so a plain `.res` suffix test excludes compiled output + interfaces.
40
+ let isSrcResFile = (name: string) => String.endsWith(name, ".res")
41
+
42
+ let rec walk = async (dir: string, acc: array<string>): array<string> => {
43
+ let entries = try {
44
+ await _readdir(dir, {withFileTypes: true})
45
+ } catch {
46
+ | _ => []
47
+ }
48
+ if isPruned(entries) {
49
+ acc
50
+ } else {
51
+ let found = ref(acc)
52
+ for i in 0 to entries->Array.length - 1 {
53
+ let entry = entries->Array.getUnsafe(i)
54
+ if !shouldIgnore(entry.name) {
55
+ let full = join(dir, entry.name)
56
+ if entry._isDirectory() {
57
+ let nested = await walk(full, [])
58
+ found := Array.concat(found.contents, nested)
59
+ } else if entry._isFile() && isSrcResFile(entry.name) {
60
+ found := Array.concat(found.contents, [full])
61
+ }
62
+ }
63
+ }
64
+ found.contents
65
+ }
66
+ }
67
+
68
+ // Enumerate components across the given package directories (deduplicated per
69
+ // dir+kind+name, declaration order preserved). Each component accumulates the
70
+ // source files that map to it (spec + body files).
71
+ type orderEntry = {dir: string, kind: string, name: string, key: string}
72
+
73
+ let scan = async (pkgDirs: array<string>): array<component> => {
74
+ let order = []
75
+ let filesByKey = Dict.make()
76
+ for i in 0 to pkgDirs->Array.length - 1 {
77
+ let dir = pkgDirs->Array.getUnsafe(i)
78
+ let srcDir = join(dir, "src")
79
+ if _existsSync(srcDir) {
80
+ let files = await walk(srcDir, [])
81
+ files->Array.forEach(file =>
82
+ switch ComponentMeta.componentOfSrcFile(file) {
83
+ | Some(c) =>
84
+ let key = dir ++ "::" ++ c.kind ++ "::" ++ c.name
85
+ switch filesByKey->Dict.get(key) {
86
+ | Some(existing) => filesByKey->Dict.set(key, Array.concat(existing, [file]))
87
+ | None => {
88
+ filesByKey->Dict.set(key, [file])
89
+ order->Array.push({dir, kind: c.kind, name: c.name, key})
90
+ }
91
+ }
92
+ | None => ()
93
+ }
94
+ )
95
+ }
96
+ }
97
+ order->Array.map(o => {
98
+ dir: o.dir,
99
+ kind: o.kind,
100
+ name: o.name,
101
+ files: filesByKey->Dict.get(o.key)->Option.getOr([]),
102
+ })
103
+ }
@@ -0,0 +1,105 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Promises from "node:fs/promises";
7
+ import * as ComponentMeta$ReventlessGwt from "./ComponentMeta.res.mjs";
8
+
9
+ let ignoreNames = [
10
+ "node_modules",
11
+ ".git",
12
+ "dist",
13
+ "lib",
14
+ ".history"
15
+ ];
16
+
17
+ function shouldIgnore(name) {
18
+ return ignoreNames.includes(name);
19
+ }
20
+
21
+ let gwtIgnoreFile = ".gwtignore";
22
+
23
+ function isPruned(entries) {
24
+ return entries.some(e => e.name === gwtIgnoreFile);
25
+ }
26
+
27
+ function isSrcResFile(name) {
28
+ return name.endsWith(".res");
29
+ }
30
+
31
+ async function walk(dir, acc) {
32
+ let entries;
33
+ try {
34
+ entries = await Promises.readdir(dir, {
35
+ withFileTypes: true
36
+ });
37
+ } catch (exn) {
38
+ entries = [];
39
+ }
40
+ if (isPruned(entries)) {
41
+ return acc;
42
+ }
43
+ let found = acc;
44
+ for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
45
+ let entry = entries[i];
46
+ if (!ignoreNames.includes(entry.name)) {
47
+ let full = Nodepath.join(dir, entry.name);
48
+ if (entry.isDirectory()) {
49
+ let nested = await walk(full, []);
50
+ found = found.concat(nested);
51
+ } else if (entry.isFile() && entry.name.endsWith(".res")) {
52
+ found = found.concat([full]);
53
+ }
54
+ }
55
+ }
56
+ return found;
57
+ }
58
+
59
+ async function scan(pkgDirs) {
60
+ let order = [];
61
+ let filesByKey = {};
62
+ for (let i = 0, i_finish = pkgDirs.length; i < i_finish; ++i) {
63
+ let dir = pkgDirs[i];
64
+ let srcDir = Nodepath.join(dir, "src");
65
+ if (Nodefs.existsSync(srcDir)) {
66
+ let files = await walk(srcDir, []);
67
+ files.forEach(file => {
68
+ let c = ComponentMeta$ReventlessGwt.componentOfSrcFile(file);
69
+ if (c === undefined) {
70
+ return;
71
+ }
72
+ let key = dir + "::" + c.kind + "::" + c.name;
73
+ let existing = filesByKey[key];
74
+ if (existing !== undefined) {
75
+ filesByKey[key] = existing.concat([file]);
76
+ } else {
77
+ filesByKey[key] = [file];
78
+ order.push({
79
+ dir: dir,
80
+ kind: c.kind,
81
+ name: c.name,
82
+ key: key
83
+ });
84
+ }
85
+ });
86
+ }
87
+ }
88
+ return order.map(o => ({
89
+ dir: o.dir,
90
+ kind: o.kind,
91
+ name: o.name,
92
+ files: Stdlib_Option.getOr(filesByKey[o.key], [])
93
+ }));
94
+ }
95
+
96
+ export {
97
+ ignoreNames,
98
+ shouldIgnore,
99
+ gwtIgnoreFile,
100
+ isPruned,
101
+ isSrcResFile,
102
+ walk,
103
+ scan,
104
+ }
105
+ /* node:fs Not a pure module */
@@ -0,0 +1,303 @@
1
+ open ReventlessCore
2
+
3
+ // Delegate_GWT — GWT for ExtensionPoint mappings and Extension delegates.
4
+ //
5
+ // Both are pure input → published-actions translations and form the one
6
+ // cross-plugin behavioural seam (per `.claude/rules/app-developer.md`). Each
7
+ // mapping has TWO directions, and each adapter drives both:
8
+ //
9
+ // * an ExtensionPoint mapping turns an internal `Delegate` event into public
10
+ // extension-point EVENTS (`mapOutgoingEvent`), and an incoming protocol
11
+ // COMMAND into a wrapped-aggregate command (`mapIncomingCommand`);
12
+ // * an Extension delegate turns an incoming extension-point EVENT into delegate
13
+ // COMMANDS (`mapIncomingEvent`), and an internal `Delegate` event into an
14
+ // outgoing protocol COMMAND (`mapOutgoingEvent`).
15
+ //
16
+ // The stimulus you inject names what arrives at the mapping, NOT what it emits:
17
+ // * `whenDelegateEvent` — an internal delegate event occurs (both adapters'
18
+ // `mapOutgoingEvent`);
19
+ // * `whenIncomingEvent` — a protocol event arrives (Extension `mapIncomingEvent`);
20
+ // * `whenIncomingCommand`— a protocol command arrives (EP `mapIncomingCommand`).
21
+ //
22
+ // This DSL drives the user-level mapping function with the inert `StubRuntime`
23
+ // handles and asserts the SET of published actions — including one-to-many
24
+ // fan-out (e.g. `OrderPlaced{productIds:[…]}` → N × `ItemOrdered`).
25
+ //
26
+ // `FromExtensionPoint(M)` / `FromExtension(M)` mirror the From* adapter pattern
27
+ // of `Mapping_GWT` / `Query_GWT`: one comparison core per direction.
28
+ //
29
+ // See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phase 1.
30
+
31
+ module EPMapping = ReventlessInfra.ExtensionPointMapping
32
+ module ExtMapping = ReventlessInfra.ExtensionMapping
33
+
34
+ // A published action normalised for comparison: a routing target plus the
35
+ // encoded payload. EP events are keyed by entity id; delegate commands by the
36
+ // component (slice/aggregate) or extension-point they route to.
37
+ let encPublished = (~target: string, payload: JSON.t): JSON.t => {
38
+ let d = Dict.make()
39
+ d->Dict.set("target", JSON.Encode.string(target))
40
+ d->Dict.set("payload", payload)
41
+ JSON.Encode.object(d)
42
+ }
43
+
44
+ // -- Comparison core ---------------------------------------------------------
45
+
46
+ module type Delegate = {
47
+ let name: string
48
+ type inbound
49
+ // Drive the underlying mapping with StubRuntime, returning the published
50
+ // actions already normalised to JSON.
51
+ let run: inbound => promise<array<JSON.t>>
52
+ }
53
+
54
+ module type T = {
55
+ type inbound
56
+
57
+ let describe: (string, unit => unit) => unit
58
+ let test: (string, ~timeout: int=?, unit => promise<Outcome.outcome>) => unit
59
+
60
+ // Feed the stimulus into the mapping; returns the normalised published actions.
61
+ // The adapters re-export this under direction-specific names (whenDelegateEvent
62
+ // / whenIncomingEvent / whenIncomingCommand).
63
+ let whenInput: inbound => promise<array<JSON.t>>
64
+ let thenPublishedJson: (promise<array<JSON.t>>, array<JSON.t>) => promise<Outcome.outcome>
65
+ let thenPublishesNothing: promise<array<JSON.t>> => promise<Outcome.outcome>
66
+ // Assert the SET of HANDLED DIRECTIVES (HandleDirective actions, encoded by the
67
+ // EP/Extension directive schema). Disjoint from thenPublishedJson — published
68
+ // events/commands and handled directives are separate channels.
69
+ let thenHandlesDirectivesJson: (promise<array<JSON.t>>, array<JSON.t>) => promise<Outcome.outcome>
70
+ }
71
+
72
+ module Make = (D: Delegate): (T with type inbound = D.inbound) => {
73
+ type inbound = D.inbound
74
+
75
+ S.enableJson()
76
+
77
+ let describe = JestBind.describe
78
+ let test = (name, ~timeout=?, body) =>
79
+ JestBind.testPromise(~slice=D.name, name, ~timeout?, body)
80
+
81
+ let whenInput = ev => D.run(ev)
82
+
83
+ // Published actions form a SET — fan-out order is non-deterministic, so the
84
+ // comparison sorts by stringified JSON before comparing.
85
+ let sortJson = (arr: array<JSON.t>) =>
86
+ arr->Array.toSorted((a, b) => String.compare(JSON.stringify(a), JSON.stringify(b)))
87
+
88
+ // Handled directives ride the same normalised array tagged `target:"directive"`
89
+ // so a single mapping run carries both channels; the publish/directive verbs
90
+ // each project to their own. (Published actions never use that target — EP/Ext
91
+ // events and commands route to an id, a component name, or `<ep>:<id>`.)
92
+ let targetOf = (j: JSON.t): string =>
93
+ j
94
+ ->JSON.Decode.object
95
+ ->Option.flatMap(d => d->Dict.get("target"))
96
+ ->Option.flatMap(JSON.Decode.string)
97
+ ->Option.getOr("")
98
+ let dropDirectives = arr => arr->Array.filter(j => targetOf(j) != "directive")
99
+ let onlyDirectives = arr => arr->Array.filter(j => targetOf(j) == "directive")
100
+
101
+ let thenPublishedJson = async (actionsP, expected) => {
102
+ let actual = (await actionsP)->dropDirectives
103
+ if sortJson(actual) == sortJson(expected) {
104
+ Outcome.pass
105
+ } else {
106
+ Outcome.fail(PublishedActionsMismatch({expected, actual}))
107
+ }
108
+ }
109
+
110
+ let thenPublishesNothing = actionsP => thenPublishedJson(actionsP, [])
111
+
112
+ let thenHandlesDirectivesJson = async (actionsP, expected) => {
113
+ let actual = (await actionsP)->onlyDirectives
114
+ if sortJson(actual) == sortJson(expected) {
115
+ Outcome.pass
116
+ } else {
117
+ Outcome.fail(PublishedActionsMismatch({expected, actual}))
118
+ }
119
+ }
120
+ }
121
+
122
+ // -- FromExtensionPoint: an EP mapping (both directions) ---------------------
123
+
124
+ module FromExtensionPoint = (M: EPMapping.Mapping) => {
125
+ let encEvent = (e: M.ExtensionPoint.event) => e->Message.encode(M.ExtensionPoint.eventSchema)
126
+ let encCommand = (c: M.Delegate.command) => c->Message.encode(M.Delegate.commandSchema)
127
+ let encDirective = (d: M.ExtensionPoint.directive) =>
128
+ encPublished(~target="directive", d->Message.encode(M.ExtensionPoint.directiveSchema))
129
+
130
+ // Internal delegate event → outgoing extension-point events (`mapOutgoingEvent`).
131
+ module EvCore = Make({
132
+ let name = M.ExtensionPoint.name
133
+ type inbound = M.Delegate.event
134
+ let run = async (event: M.Delegate.event) =>
135
+ switch M.mapOutgoingEvent {
136
+ | None => []
137
+ | Some(f) =>
138
+ let nested =
139
+ await f("gwt-id", event, StubRuntime.meta, StubRuntime.queryEngine)
140
+ ->Array.map(async action =>
141
+ switch action {
142
+ | EPMapping.PublishEvent(id, ev) => [encPublished(~target=id, encEvent(ev))]
143
+ | EPMapping.PublishEventAsync(p) =>
144
+ let (id, ev) = await p
145
+ [encPublished(~target=id, encEvent(ev))]
146
+ | EPMapping.HandleDirective(_, dir) => [encDirective(dir)]
147
+ }
148
+ )
149
+ ->Promise.all
150
+ nested->Array.flat
151
+ }
152
+ })
153
+
154
+ // Incoming extension-point command → wrapped-aggregate commands (`mapIncomingCommand`).
155
+ module CmdCore = Make({
156
+ let name = M.ExtensionPoint.name
157
+ type inbound = M.ExtensionPoint.command
158
+ let run = async (command: M.ExtensionPoint.command) =>
159
+ M.mapIncomingCommand("gwt-id", command, StubRuntime.meta)->Array.filterMap(action =>
160
+ switch action {
161
+ | EPMapping.PublishCommand(id, cmd) => Some(encPublished(~target=id, encCommand(cmd)))
162
+ | EPMapping.HandleDirective(_, dir) => Some(encDirective(dir))
163
+ }
164
+ )
165
+ })
166
+
167
+ type inbound = M.Delegate.event
168
+ let describe = EvCore.describe
169
+ let test = EvCore.test
170
+ // `thenPublishesNothing` is direction-agnostic (compares to []), usable after
171
+ // either `whenDelegateEvent` or `whenIncomingCommand`.
172
+ let thenPublishesNothing = EvCore.thenPublishesNothing
173
+
174
+ // Delegate event → outgoing protocol events.
175
+ let whenDelegateEvent = EvCore.whenInput
176
+ let thenPublishesEvent = (actionsP, id, event: M.ExtensionPoint.event) =>
177
+ EvCore.thenPublishedJson(actionsP, [encPublished(~target=id, encEvent(event))])
178
+ let thenPublishesEvents = (actionsP, pairs: array<(string, M.ExtensionPoint.event)>) =>
179
+ EvCore.thenPublishedJson(
180
+ actionsP,
181
+ pairs->Array.map(((id, ev)) => encPublished(~target=id, encEvent(ev))),
182
+ )
183
+
184
+ // Incoming protocol command → wrapped-aggregate commands.
185
+ let whenIncomingCommand = CmdCore.whenInput
186
+ let thenPublishesCommand = (actionsP, id, cmd: M.Delegate.command) =>
187
+ CmdCore.thenPublishedJson(actionsP, [encPublished(~target=id, encCommand(cmd))])
188
+ let thenPublishesCommands = (actionsP, pairs: array<(string, M.Delegate.command)>) =>
189
+ CmdCore.thenPublishedJson(
190
+ actionsP,
191
+ pairs->Array.map(((id, cmd)) => encPublished(~target=id, encCommand(cmd))),
192
+ )
193
+
194
+ // Handled directives — usable after either `when`; both cores tag directives on
195
+ // the same channel, so this projects them out regardless of direction.
196
+ let thenHandlesDirective = (actionsP, directive: M.ExtensionPoint.directive) =>
197
+ EvCore.thenHandlesDirectivesJson(actionsP, [encDirective(directive)])
198
+ let thenHandlesDirectives = (actionsP, directives: array<M.ExtensionPoint.directive>) =>
199
+ EvCore.thenHandlesDirectivesJson(actionsP, directives->Array.map(encDirective))
200
+ let thenHandlesNoDirective = actionsP => EvCore.thenHandlesDirectivesJson(actionsP, [])
201
+ }
202
+
203
+ // -- FromExtension: an Extension delegate (both directions) ------------------
204
+
205
+ module FromExtension = (M: ExtMapping.Mapping) => {
206
+ let encCmd = (c: M.Delegate.command) => c->Message.encode(M.Delegate.commandSchema)
207
+ let encEpCmd = (c: M.ExtensionPoint.command) => c->Message.encode(M.ExtensionPoint.commandSchema)
208
+ let encDirective = (d: M.ExtensionPoint.directive) =>
209
+ encPublished(~target="directive", d->Message.encode(M.ExtensionPoint.directiveSchema))
210
+
211
+ // Incoming extension-point event → delegate commands (`mapIncomingEvent`).
212
+ module EvCore = Make({
213
+ let name = M.Delegate.name
214
+ type inbound = M.ExtensionPoint.event
215
+ let run = async (event: M.ExtensionPoint.event) => {
216
+ let nested =
217
+ await M.mapIncomingEvent(
218
+ "gwt-id",
219
+ event,
220
+ StubRuntime.meta,
221
+ StubRuntime.pluginDefinition,
222
+ StubRuntime.queryEngine,
223
+ )
224
+ ->Array.map(async action =>
225
+ switch action {
226
+ | ExtMapping.PublishStateChangeSliceCommand(cmd) => [
227
+ encPublished(~target=M.Delegate.name, encCmd(cmd)),
228
+ ]
229
+ | ExtMapping.PublishStateChangeSliceCommandAsync(p) =>
230
+ let cmd = await p
231
+ [encPublished(~target=M.Delegate.name, encCmd(cmd))]
232
+ | ExtMapping.PublishStateChangeSliceCommandsAsync(p) =>
233
+ (await p)->Array.map(cmd => encPublished(~target=M.Delegate.name, encCmd(cmd)))
234
+ | ExtMapping.PublishAggregateCommand(id, cmd) => [encPublished(~target=id, encCmd(cmd))]
235
+ | ExtMapping.PublishAggregateCommandAsync(p) =>
236
+ let (id, cmd) = await p
237
+ [encPublished(~target=id, encCmd(cmd))]
238
+ | ExtMapping.PublishAggregateCommandsAsync(p) =>
239
+ (await p)->Array.map(((id, cmd)) => encPublished(~target=id, encCmd(cmd)))
240
+ | ExtMapping.PublishExtensionPointCommand(id, cmd) => [
241
+ encPublished(~target=id, encEpCmd(cmd)),
242
+ ]
243
+ | ExtMapping.ForwardCommand({extensionPointName, id, commandJson}) => [
244
+ encPublished(~target=`${extensionPointName}:${id}`, commandJson),
245
+ ]
246
+ | ExtMapping.HandleDirective(_, dir) => [encDirective(dir)]
247
+ }
248
+ )
249
+ ->Promise.all
250
+ nested->Array.flat
251
+ }
252
+ })
253
+
254
+ // Internal delegate event → outgoing protocol commands (`mapOutgoingEvent`, optional).
255
+ module OutCore = Make({
256
+ let name = M.Delegate.name
257
+ type inbound = M.Delegate.event
258
+ let run = async (event: M.Delegate.event) =>
259
+ switch M.mapOutgoingEvent {
260
+ | None => []
261
+ | Some(f) =>
262
+ f("gwt-id", event, StubRuntime.meta, StubRuntime.pluginDefinition)->Array.filterMap(action =>
263
+ switch action {
264
+ | ExtMapping.PublishExtensionPointCommand(id, cmd) =>
265
+ Some(encPublished(~target=id, encEpCmd(cmd)))
266
+ | ExtMapping.ForwardCommand({extensionPointName, id, commandJson}) =>
267
+ Some(encPublished(~target=`${extensionPointName}:${id}`, commandJson))
268
+ | ExtMapping.HandleDirective(_, dir) => Some(encDirective(dir))
269
+ }
270
+ )
271
+ }
272
+ })
273
+
274
+ type inbound = M.ExtensionPoint.event
275
+ let describe = EvCore.describe
276
+ let test = EvCore.test
277
+ let thenPublishesNothing = EvCore.thenPublishesNothing
278
+
279
+ // Incoming protocol event → delegate commands.
280
+ let whenIncomingEvent = EvCore.whenInput
281
+ let thenPublishesCommand = (actionsP, cmd: M.Delegate.command) =>
282
+ EvCore.thenPublishedJson(actionsP, [encPublished(~target=M.Delegate.name, encCmd(cmd))])
283
+ let thenPublishesCommands = (actionsP, cmds: array<M.Delegate.command>) =>
284
+ EvCore.thenPublishedJson(
285
+ actionsP,
286
+ cmds->Array.map(cmd => encPublished(~target=M.Delegate.name, encCmd(cmd))),
287
+ )
288
+ let thenPublishesAggregateCommand = (actionsP, id, cmd: M.Delegate.command) =>
289
+ EvCore.thenPublishedJson(actionsP, [encPublished(~target=id, encCmd(cmd))])
290
+
291
+ // Internal delegate event → outgoing protocol commands.
292
+ let whenDelegateEvent = OutCore.whenInput
293
+ let thenPublishesExtensionPointCommand = (actionsP, id, cmd: M.ExtensionPoint.command) =>
294
+ OutCore.thenPublishedJson(actionsP, [encPublished(~target=id, encEpCmd(cmd))])
295
+
296
+ // Handled directives — usable after either `when` (both cores tag directives on
297
+ // the same channel).
298
+ let thenHandlesDirective = (actionsP, directive: M.ExtensionPoint.directive) =>
299
+ EvCore.thenHandlesDirectivesJson(actionsP, [encDirective(directive)])
300
+ let thenHandlesDirectives = (actionsP, directives: array<M.ExtensionPoint.directive>) =>
301
+ EvCore.thenHandlesDirectivesJson(actionsP, directives->Array.map(encDirective))
302
+ let thenHandlesNoDirective = actionsP => EvCore.thenHandlesDirectivesJson(actionsP, [])
303
+ }