@reventlessdev/reventless-gwt 1.0.0-alpha.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CHANGELOG.md +916 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +569 -0
  10. package/src/Behavior_GWT.res.mjs +523 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +147 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +37 -0
  16. package/src/Cancellation.res.mjs +46 -0
  17. package/src/ChildProcess.res +102 -0
  18. package/src/ChildProcess.res.mjs +107 -0
  19. package/src/Cli.res +905 -0
  20. package/src/Cli.res.mjs +952 -0
  21. package/src/Collector.res +212 -0
  22. package/src/Collector.res.mjs +218 -0
  23. package/src/ComponentMeta.res +77 -0
  24. package/src/ComponentMeta.res.mjs +91 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +99 -0
  33. package/src/DomainDeadCode.res +98 -0
  34. package/src/DomainDeadCode.res.mjs +72 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +45 -0
  40. package/src/Filter.res.mjs +47 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +109 -0
  44. package/src/FormatterHuman.res.mjs +115 -0
  45. package/src/FormatterJson.res +264 -0
  46. package/src/FormatterJson.res.mjs +322 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +100 -0
  50. package/src/FormatterTap.res.mjs +131 -0
  51. package/src/FormatterVsCode.res +352 -0
  52. package/src/FormatterVsCode.res.mjs +546 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +119 -0
  62. package/src/LocalHost.res.mjs +99 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MismatchRender.res +72 -0
  66. package/src/MismatchRender.res.mjs +127 -0
  67. package/src/MultiSourceProjection_GWT.res +316 -0
  68. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  69. package/src/OutboundTranslation_GWT.res +238 -0
  70. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  71. package/src/Outcome.res +97 -0
  72. package/src/Outcome.res.mjs +93 -0
  73. package/src/PackageScan.res +80 -0
  74. package/src/PackageScan.res.mjs +81 -0
  75. package/src/PlatformRunner.res +181 -0
  76. package/src/PlatformRunner.res.mjs +176 -0
  77. package/src/PlatformScan.res +142 -0
  78. package/src/PlatformScan.res.mjs +132 -0
  79. package/src/ProcessManager.res +135 -0
  80. package/src/ProcessManager.res.mjs +119 -0
  81. package/src/Projection_GWT.res +312 -0
  82. package/src/Projection_GWT.res.mjs +332 -0
  83. package/src/Query_GWT.res +379 -0
  84. package/src/Query_GWT.res.mjs +367 -0
  85. package/src/RenderRescript.res +125 -0
  86. package/src/RenderRescript.res.mjs +141 -0
  87. package/src/RunWorker.res +24 -0
  88. package/src/RunWorker.res.mjs +16 -0
  89. package/src/RunnerTypes.res +68 -0
  90. package/src/RunnerTypes.res.mjs +60 -0
  91. package/src/SideEffect_GWT.res +157 -0
  92. package/src/SideEffect_GWT.res.mjs +113 -0
  93. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  94. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  95. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  96. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  97. package/src/StateChange/WithFixtures.res +22 -0
  98. package/src/StateChange/WithFixtures.res.mjs +31 -0
  99. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  100. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  101. package/src/StateChange/WithManualOpen.res +23 -0
  102. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  103. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  104. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  105. package/src/StubRuntime.res +54 -0
  106. package/src/StubRuntime.res.mjs +55 -0
  107. package/src/TestFixtures.res +16 -0
  108. package/src/TestFixtures.res.mjs +34 -0
  109. package/src/Watch.res +108 -0
  110. package/src/Watch.res.mjs +85 -0
  111. package/src/WatcherProbe.res +56 -0
  112. package/src/WatcherProbe.res.mjs +59 -0
  113. package/src/Worker.res +24 -0
  114. package/src/Worker.res.mjs +9 -0
  115. package/tests/AutomationGwtTest.res +71 -0
  116. package/tests/AutomationGwtTest.res.mjs +189 -0
  117. package/tests/BuildClassifierTest.res +133 -0
  118. package/tests/BuildClassifierTest.res.mjs +176 -0
  119. package/tests/CliRunEntryTest.res +57 -0
  120. package/tests/CliRunEntryTest.res.mjs +65 -0
  121. package/tests/CliWatchScopeTest.res +62 -0
  122. package/tests/CliWatchScopeTest.res.mjs +95 -0
  123. package/tests/ComponentMetaTest.res +67 -0
  124. package/tests/ComponentMetaTest.res.mjs +71 -0
  125. package/tests/DelegateGwtTest.res +487 -0
  126. package/tests/DelegateGwtTest.res.mjs +891 -0
  127. package/tests/DiscoveryTest.res +42 -0
  128. package/tests/DiscoveryTest.res.mjs +43 -0
  129. package/tests/DomainDeadCodeTest.res +119 -0
  130. package/tests/DomainDeadCodeTest.res.mjs +158 -0
  131. package/tests/DomainGraphTest.res +448 -0
  132. package/tests/DomainGraphTest.res.mjs +505 -0
  133. package/tests/FlowAggregateGwtTest.res +171 -0
  134. package/tests/FlowAggregateGwtTest.res.mjs +273 -0
  135. package/tests/FlowCrossPluginGwtTest.res +340 -0
  136. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  137. package/tests/FlowGwtTest.res +222 -0
  138. package/tests/FlowGwtTest.res.mjs +433 -0
  139. package/tests/FormatterGoldenTest.res +125 -0
  140. package/tests/FormatterGoldenTest.res.mjs +162 -0
  141. package/tests/FormatterVsCodeMessageTest.res +41 -0
  142. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  143. package/tests/InboundTranslationGwtTest.res +34 -0
  144. package/tests/InboundTranslationGwtTest.res.mjs +99 -0
  145. package/tests/LocalHostIntegration.res +70 -0
  146. package/tests/LocalHostIntegration.res.mjs +64 -0
  147. package/tests/LocalHostTest.res +71 -0
  148. package/tests/LocalHostTest.res.mjs +63 -0
  149. package/tests/MappingGwtTest.res +350 -0
  150. package/tests/MappingGwtTest.res.mjs +596 -0
  151. package/tests/OutboundTranslationGwtTest.res +59 -0
  152. package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
  153. package/tests/PlatformRunnerTest.res +66 -0
  154. package/tests/PlatformRunnerTest.res.mjs +85 -0
  155. package/tests/PlatformScanTest.res +61 -0
  156. package/tests/PlatformScanTest.res.mjs +56 -0
  157. package/tests/QueryGwtTest.res +161 -0
  158. package/tests/QueryGwtTest.res.mjs +314 -0
  159. package/tests/QueryResolverGwtTest.res +71 -0
  160. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  161. package/tests/RunnerUnitTest.res +219 -0
  162. package/tests/RunnerUnitTest.res.mjs +244 -0
  163. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  164. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  165. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  166. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  167. package/tests/SideEffect/MockEmail.res +18 -0
  168. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  169. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  170. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  171. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  172. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  173. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  174. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  175. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  176. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  177. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  178. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  179. package/tests/StateChangeSliceGwtTest.res +160 -0
  180. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  181. package/tests/StateViewSliceGwtTest.res +64 -0
  182. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  183. package/tests/WatchDebounceTest.res +106 -0
  184. package/tests/WatchDebounceTest.res.mjs +128 -0
@@ -0,0 +1,132 @@
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_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Promises from "node:fs/promises";
8
+
9
+ let localPlatformDep = "@reventlessdev/reventless-local";
10
+
11
+ let ignoreNames = [
12
+ "node_modules",
13
+ ".git",
14
+ "dist",
15
+ "lib",
16
+ ".history"
17
+ ];
18
+
19
+ function matchPlatform(pkgJsonText, mainExists) {
20
+ if (!mainExists) {
21
+ return;
22
+ }
23
+ let json;
24
+ try {
25
+ json = JSON.parse(pkgJsonText);
26
+ } catch (exn) {
27
+ return;
28
+ }
29
+ let obj = Stdlib_JSON.Decode.object(json);
30
+ if (obj === undefined) {
31
+ return;
32
+ }
33
+ let match = obj["name"];
34
+ let name = typeof match === "string" ? match : "";
35
+ let depsHave = key => {
36
+ let deps = Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.object);
37
+ if (deps !== undefined) {
38
+ return Stdlib_Option.isSome(deps[localPlatformDep]);
39
+ } else {
40
+ return false;
41
+ }
42
+ };
43
+ let dependsOnLocal = depsHave("dependencies") || depsHave("devDependencies") || depsHave("optionalDependencies");
44
+ if (!dependsOnLocal) {
45
+ return;
46
+ }
47
+ let scripts = Stdlib_Option.flatMap(obj["scripts"], Stdlib_JSON.Decode.object);
48
+ let serveScript = scripts !== undefined ? (
49
+ Stdlib_Option.isSome(scripts["serve:memory"]) ? "serve:memory" : (
50
+ Stdlib_Option.isSome(scripts["serve"]) ? "serve" : undefined
51
+ )
52
+ ) : undefined;
53
+ return [
54
+ name,
55
+ serveScript
56
+ ];
57
+ }
58
+
59
+ function inspectDir(dir) {
60
+ let pkgJson = Nodepath.join(dir, "package.json");
61
+ let mainPath = Nodepath.join(Nodepath.join(dir, "src"), "Main.res.mjs");
62
+ if (!Nodefs.existsSync(pkgJson)) {
63
+ return;
64
+ }
65
+ let text;
66
+ try {
67
+ text = Nodefs.readFileSync(pkgJson, "utf8");
68
+ } catch (exn) {
69
+ text = "";
70
+ }
71
+ let match = matchPlatform(text, Nodefs.existsSync(mainPath));
72
+ if (match !== undefined) {
73
+ return {
74
+ name: match[0],
75
+ dir: dir,
76
+ mainPath: mainPath,
77
+ serveScript: match[1]
78
+ };
79
+ }
80
+ }
81
+
82
+ async function walk(dir, acc) {
83
+ let pkg = inspectDir(dir);
84
+ if (pkg !== undefined) {
85
+ acc.push(pkg);
86
+ } else {
87
+ let entries;
88
+ try {
89
+ entries = await Promises.readdir(dir, {
90
+ withFileTypes: true
91
+ });
92
+ } catch (exn) {
93
+ entries = [];
94
+ }
95
+ for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
96
+ let entry = entries[i];
97
+ if (entry.isDirectory() && !ignoreNames.includes(entry.name)) {
98
+ await walk(Nodepath.join(dir, entry.name), acc);
99
+ }
100
+ }
101
+ }
102
+ return acc;
103
+ }
104
+
105
+ async function scan(roots) {
106
+ let seen = {};
107
+ let out = [];
108
+ for (let i = 0, i_finish = roots.length; i < i_finish; ++i) {
109
+ let found = await walk(roots[i], []);
110
+ found.forEach(pkg => {
111
+ let match = seen[pkg.dir];
112
+ if (match !== undefined) {
113
+ return;
114
+ } else {
115
+ seen[pkg.dir] = true;
116
+ out.push(pkg);
117
+ return;
118
+ }
119
+ });
120
+ }
121
+ return out;
122
+ }
123
+
124
+ export {
125
+ localPlatformDep,
126
+ ignoreNames,
127
+ matchPlatform,
128
+ inspectDir,
129
+ walk,
130
+ scan,
131
+ }
132
+ /* node:fs Not a pure module */
@@ -0,0 +1,135 @@
1
+ // Owns the `rescript build -w` watchers a watch session spawns — one per
2
+ // package that owns tests (PackageScan), at most one per directory,
3
+ // reference-counted by `dir`. Registered with Cancellation so SIGINT/SIGTERM
4
+ // tears down every spawned watcher; never orphans a compiler.
5
+
6
+ type managed = {
7
+ dir: string,
8
+ proc: ChildProcess.t,
9
+ }
10
+
11
+ let running: Dict.t<managed> = Dict.make()
12
+
13
+ // Per-dir count of unexpected watcher exits, so a build-watcher that crashes on
14
+ // startup (bad config, missing binary) is respawned a bounded number of times
15
+ // rather than either freezing the build status forever or spinning in a tight
16
+ // respawn loop.
17
+ let restarts: Dict.t<int> = Dict.make()
18
+ let maxRestarts = 3
19
+
20
+ let isManaging = (dir: string): bool =>
21
+ switch running->Dict.get(dir) {
22
+ | Some(_) => true
23
+ | None => false
24
+ }
25
+
26
+ // Spawn `pnpm run start` (the package's `rescript build -w`) in `pkg.dir`,
27
+ // unless one is already managed there. `onStdout` receives each output line so
28
+ // the caller can classify compiler markers (Phase 5).
29
+ let rec spawnWatcher = (pkg: PackageScan.pkg, ~onStdout: (string, string) => unit): unit =>
30
+ switch running->Dict.get(pkg.dir) {
31
+ | Some(_) => ()
32
+ | None => {
33
+ let proc = ChildProcess.spawn("pnpm", ["run", "start"], ~cwd=pkg.dir)
34
+ ChildProcess.onStdoutLine(proc, line => onStdout(pkg.dir, line))
35
+ // stderr carries compiler/pnpm crash output; without this a watcher that
36
+ // dies on stderr was invisible until the caller's 120s watchdog.
37
+ ChildProcess.onStderrLine(proc, line => onStdout(pkg.dir, line))
38
+ // A spawn failure ("error") never reaches "close"; surface it as output.
39
+ ChildProcess.onError(proc, msg => onStdout(pkg.dir, msg))
40
+ // Detect an unexpected exit. We compare the stored proc identity: an
41
+ // intentional `stop` deletes the entry synchronously before "close" fires,
42
+ // so if we still own this exact proc, the watcher crashed — respawn it up
43
+ // to `maxRestarts` times so build status doesn't freeze.
44
+ ChildProcess.onClose(proc, _code =>
45
+ switch running->Dict.get(pkg.dir) {
46
+ | Some(m) if m.proc === proc =>
47
+ running->Dict.delete(pkg.dir)
48
+ let n = restarts->Dict.get(pkg.dir)->Option.getOr(0)
49
+ if n < maxRestarts {
50
+ restarts->Dict.set(pkg.dir, n + 1)
51
+ spawnWatcher(pkg, ~onStdout)
52
+ }
53
+ | _ => ()
54
+ }
55
+ )
56
+ running->Dict.set(pkg.dir, {dir: pkg.dir, proc})
57
+ }
58
+ }
59
+
60
+ // Stop just the watcher managed for `dir` (if any), tearing down its process
61
+ // group. Returns true when one was running — i.e. the caller *owned* this
62
+ // package's build (vs. an adopted external watcher, which we can't stop).
63
+ let stop = (dir: string): bool =>
64
+ switch running->Dict.get(dir) {
65
+ | Some(m) => {
66
+ ChildProcess.killTree(m.proc, "SIGTERM")
67
+ running->Dict.delete(dir)
68
+ true
69
+ }
70
+ | None => false
71
+ }
72
+
73
+ // Force a full re-emit of `pkg` after a structural change — a module relocation
74
+ // that incremental compilation can't detect, since it keys invalidation on
75
+ // source + dependency-interface hashes, never on a dependency's filesystem
76
+ // location (see plan Phase 12). Drives a deterministic one-shot `rescript clean`
77
+ // → `rescript build` to completion (the `clean` is what forces every dependent
78
+ // `.res.mjs` to be re-emitted against the new layout; the `build` is essential
79
+ // even on the adopted path — an external watcher won't rebuild after a clean
80
+ // without a fresh source change, so a bare clean would strand the package with
81
+ // no outputs). If we *owned* the package's watcher we stop it first (no lock
82
+ // contention) and respawn it afterward to resume incremental watching; on the
83
+ // adopted path the developer's own watcher keeps running untouched. `onStdout`
84
+ // streams the clean/build output (compiler markers) to the caller; `onDone`
85
+ // fires once the build completes, gating the test re-run.
86
+ let cleanRebuild = (
87
+ pkg: PackageScan.pkg,
88
+ ~onStdout: (string, string) => unit,
89
+ ~onDone: bool => unit,
90
+ ): unit => {
91
+ let wasManaged = stop(pkg.dir)
92
+ // `next` receives the step's exit code so the chain can tell a failed clean or
93
+ // build (non-zero / crash) from success — the previous shape discarded exit
94
+ // codes and always reported the rebuild as OK, masking compile failures.
95
+ let step = (cmd: string, args: array<string>, next: option<int> => unit): unit => {
96
+ let proc = ChildProcess.spawn(cmd, args, ~cwd=pkg.dir)
97
+ // "error" (spawn failure) and "close" can both fire; ensure the chain
98
+ // advances exactly once.
99
+ let settled = ref(false)
100
+ let settle = (code: option<int>) =>
101
+ if !settled.contents {
102
+ settled := true
103
+ next(code)
104
+ }
105
+ ChildProcess.onStdoutLine(proc, line => onStdout(pkg.dir, line))
106
+ ChildProcess.onStderrLine(proc, line => onStdout(pkg.dir, line))
107
+ ChildProcess.onError(proc, msg => {
108
+ onStdout(pkg.dir, msg)
109
+ settle(None)
110
+ })
111
+ ChildProcess.onClose(proc, code => settle(code))
112
+ }
113
+ step("pnpm", ["exec", "rescript", "clean"], _cleanCode =>
114
+ step("pnpm", ["exec", "rescript", "build"], buildCode => {
115
+ if wasManaged {
116
+ spawnWatcher(pkg, ~onStdout)
117
+ }
118
+ onDone(buildCode == Some(0))
119
+ })
120
+ )
121
+ }
122
+
123
+ // SIGTERM, not SIGINT: a POSIX shell sets background children to ignore SIGINT,
124
+ // so SIGINT can leak a watcher; SIGTERM tears down the whole group reliably
125
+ // (verified across foreground and background process-tree shapes).
126
+ let killAll = (): unit => {
127
+ running
128
+ ->Dict.valuesToArray
129
+ ->Array.forEach(m => ChildProcess.killTree(m.proc, "SIGTERM"))
130
+ running
131
+ ->Dict.keysToArray
132
+ ->Array.forEach(k => running->Dict.delete(k))
133
+ }
134
+
135
+ let managedCount = (): int => running->Dict.keysToArray->Array.length
@@ -0,0 +1,119 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
+ import * as ChildProcess$ReventlessGwt from "./ChildProcess.res.mjs";
7
+
8
+ let running = {};
9
+
10
+ let restarts = {};
11
+
12
+ function isManaging(dir) {
13
+ return running[dir] !== undefined;
14
+ }
15
+
16
+ function spawnWatcher(pkg, onStdout) {
17
+ let match = running[pkg.dir];
18
+ if (match !== undefined) {
19
+ return;
20
+ }
21
+ let proc = ChildProcess$ReventlessGwt.spawn("pnpm", [
22
+ "run",
23
+ "start"
24
+ ], pkg.dir, undefined);
25
+ ChildProcess$ReventlessGwt.onStdoutLine(proc, line => onStdout(pkg.dir, line));
26
+ ChildProcess$ReventlessGwt.onStderrLine(proc, line => onStdout(pkg.dir, line));
27
+ ChildProcess$ReventlessGwt.onError(proc, msg => onStdout(pkg.dir, msg));
28
+ ChildProcess$ReventlessGwt.onClose(proc, _code => {
29
+ let m = running[pkg.dir];
30
+ if (m === undefined) {
31
+ return;
32
+ }
33
+ if (m.proc !== proc) {
34
+ return;
35
+ }
36
+ Stdlib_Dict.$$delete(running, pkg.dir);
37
+ let n = Stdlib_Option.getOr(restarts[pkg.dir], 0);
38
+ if (n < 3) {
39
+ restarts[pkg.dir] = n + 1 | 0;
40
+ return spawnWatcher(pkg, onStdout);
41
+ }
42
+ });
43
+ running[pkg.dir] = {
44
+ dir: pkg.dir,
45
+ proc: proc
46
+ };
47
+ }
48
+
49
+ function stop(dir) {
50
+ let m = running[dir];
51
+ if (m !== undefined) {
52
+ ChildProcess$ReventlessGwt.killTree(m.proc, "SIGTERM");
53
+ Stdlib_Dict.$$delete(running, dir);
54
+ return true;
55
+ } else {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function cleanRebuild(pkg, onStdout, onDone) {
61
+ let wasManaged = stop(pkg.dir);
62
+ let step = (cmd, args, next) => {
63
+ let proc = ChildProcess$ReventlessGwt.spawn(cmd, args, pkg.dir, undefined);
64
+ let settled = {
65
+ contents: false
66
+ };
67
+ let settle = code => {
68
+ if (!settled.contents) {
69
+ settled.contents = true;
70
+ return next(code);
71
+ }
72
+ };
73
+ ChildProcess$ReventlessGwt.onStdoutLine(proc, line => onStdout(pkg.dir, line));
74
+ ChildProcess$ReventlessGwt.onStderrLine(proc, line => onStdout(pkg.dir, line));
75
+ ChildProcess$ReventlessGwt.onError(proc, msg => {
76
+ onStdout(pkg.dir, msg);
77
+ settle(undefined);
78
+ });
79
+ ChildProcess$ReventlessGwt.onClose(proc, settle);
80
+ };
81
+ step("pnpm", [
82
+ "exec",
83
+ "rescript",
84
+ "clean"
85
+ ], _cleanCode => step("pnpm", [
86
+ "exec",
87
+ "rescript",
88
+ "build"
89
+ ], buildCode => {
90
+ if (wasManaged) {
91
+ spawnWatcher(pkg, onStdout);
92
+ }
93
+ onDone(Primitive_object.equal(buildCode, 0));
94
+ }));
95
+ }
96
+
97
+ function killAll() {
98
+ Object.values(running).forEach(m => ChildProcess$ReventlessGwt.killTree(m.proc, "SIGTERM"));
99
+ Object.keys(running).forEach(k => Stdlib_Dict.$$delete(running, k));
100
+ }
101
+
102
+ function managedCount() {
103
+ return Object.keys(running).length;
104
+ }
105
+
106
+ let maxRestarts = 3;
107
+
108
+ export {
109
+ running,
110
+ restarts,
111
+ maxRestarts,
112
+ isManaging,
113
+ spawnWatcher,
114
+ stop,
115
+ cleanRebuild,
116
+ killAll,
117
+ managedCount,
118
+ }
119
+ /* ChildProcess-ReventlessGwt Not a pure module */
@@ -0,0 +1,312 @@
1
+ open ReventlessCore
2
+
3
+ // Plan 02 — single-source projection GWT for StateViewSlice (and any future
4
+ // single-source projection target). Promoted from the legacy
5
+ // `StateViewSlice_GWT` so the DSL name now matches the implementation kind
6
+ // emitted by the PPX (`Projection_GWT.Make(Spec, X_Projection)`).
7
+ //
8
+ // Multi-source ReadModel projections (the `FooProjections.res + Mapping.Make`
9
+ // pattern) live in `MultiSourceProjection_GWT` — same DSL surface, different
10
+ // source description.
11
+
12
+ // Minimal inline Spec — mirrors `Reventless.StateViewSlice.Spec` minus the
13
+ // fields the GWT DSL doesn't read (`config`). Declared inline so sury-ppx
14
+ // processes `@schema` in this compilation unit.
15
+ module type Spec = {
16
+ let name: string
17
+
18
+ @schema
19
+ type state
20
+
21
+ let stateSchema: S.t<state>
22
+
23
+ @schema
24
+ type consumedEvent
25
+
26
+ let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>>
27
+ }
28
+
29
+ // Implementation half — one binding (`project`).
30
+ module type Projection = {
31
+ module Spec: Spec
32
+
33
+ let project: Spec.consumedEvent => array<Reventless.Projection.action<string, Spec.state>>
34
+ }
35
+
36
+ module type T = {
37
+ module Spec: Spec
38
+
39
+ let describe: (string, unit => unit) => unit
40
+ let describeWithId: (string, string, unit => unit) => unit
41
+ let todo: string => unit
42
+ let test: (string, ~timeout: int=?, unit => promise<Outcome.outcome>) => unit
43
+
44
+ type store = dict<array<Spec.state>>
45
+ type storeThunk = unit => promise<store>
46
+
47
+ let givenEvents: array<Spec.consumedEvent> => promise<store>
48
+ let whenEvent: (promise<store>, Spec.consumedEvent) => storeThunk
49
+ let whenEvents: (promise<store>, array<Spec.consumedEvent>) => storeThunk
50
+ let thenStates: (storeThunk, array<Spec.state>) => promise<Outcome.outcome>
51
+ let thenStatesWithId: (storeThunk, string, array<Spec.state>) => promise<Outcome.outcome>
52
+ let thenAllStates: (storeThunk, store) => promise<Outcome.outcome>
53
+ let thenState: (storeThunk, Spec.state) => promise<Outcome.outcome>
54
+ let thenStateWithId: (storeThunk, string, Spec.state) => promise<Outcome.outcome>
55
+ let thenNoState: storeThunk => promise<Outcome.outcome>
56
+ let thenThrow: storeThunk => promise<Outcome.outcome>
57
+ let thenFail: storeThunk => promise<Outcome.outcome>
58
+ }
59
+
60
+ let handleActions = Projection.handleActions // local alias to avoid shadowing
61
+
62
+ module Make = (
63
+ Spec: Spec,
64
+ Projection: Projection with module Spec := Spec,
65
+ ): (T with module Spec = Spec) => {
66
+ module Spec = Spec
67
+
68
+ S.enableJson()
69
+
70
+ let testId = ref(TestFixtures.id)
71
+
72
+ let describe = JestBind.describe
73
+ let todo = JestBind.todo
74
+ let describeWithId = (description, id, fn) => {
75
+ testId := id
76
+ JestBind.describe(description, fn)
77
+ }
78
+ let test = (name, ~timeout=?, body) =>
79
+ JestBind.testPromise(~slice=Spec.name, name, ~timeout?, body)
80
+
81
+ type store = dict<array<Spec.state>>
82
+ type storeThunk = unit => promise<store>
83
+
84
+ let getSubId = state => Spec.subIdConfig->Option.map(({getSubId}) => state->getSubId)
85
+ let hasSubId = (state, subId) => state->getSubId->Option.getOrThrow == subId
86
+ let states = (store, id) => store->Dict.get(id)->Option.getOr([])
87
+ let setStates = (store, id, states) => store->Dict.set(id, states)
88
+ let updateState = (store, id, subId, newState) =>
89
+ store->states(id)->Array.map(state => state->hasSubId(subId) ? newState : state)
90
+ let addState = (store, id, newState) => {
91
+ let (updatedStates, newStates) = switch newState->getSubId {
92
+ | Some(subId) =>
93
+ store->states(id)->Array.some(state => state->hasSubId(subId))
94
+ ? (store->updateState(id, subId, newState), [])
95
+ : (store->states(id), [newState])
96
+ | None => (store->states(id), [newState])
97
+ }
98
+ store->Dict.set(id, Array.concat(updatedStates, newStates))
99
+ }
100
+ let deleteStates = (store, id) => store->Dict.set(id, [])
101
+ let deleteSubState = (store, id, subId, getSubId) =>
102
+ store->Dict.set(
103
+ id,
104
+ store
105
+ ->Dict.get(id)
106
+ ->Option.map(states => states->Array.filter(state => state->getSubId != subId))
107
+ ->Option.getOr([]),
108
+ )
109
+
110
+ let load = (store, id) => store->states(id)->Ok->Promise.resolve
111
+ let save = (store, id, state, saveMode: QueryDb.saveMode, _ttl) =>
112
+ switch (store->states(id), saveMode, Spec.subIdConfig) {
113
+ | (_, Any, Some(_))
114
+ | (_, Init, Some(_)) =>
115
+ store->addState(id, state)
116
+ Ok()->Promise.resolve
117
+ | (_, Any, None)
118
+ | ([], Init, _)
119
+ | ([_], Overwrite, _) =>
120
+ store->setStates(id, [state])
121
+ Ok()->Promise.resolve
122
+ | _ => Error(ReventlessInfra.QueryDb.StaleState)->Promise.resolve
123
+ }
124
+ let saveBatch = (store, batch) => {
125
+ batch->Array.forEach(((id, state, _ttl)) => store->addState(id, state))
126
+ Ok()->Promise.resolve
127
+ }
128
+ let delete = (store, id, subId) =>
129
+ switch (subId, Spec.subIdConfig) {
130
+ | (None, _) =>
131
+ store->deleteStates(id)
132
+ Ok()->Promise.resolve
133
+ | (Some((_, subId)), Some({Reventless.ReadModel.getSubId: getSubId})) =>
134
+ store->deleteSubState(id, subId, getSubId)
135
+ Ok()->Promise.resolve
136
+ | _ => Ok()->Promise.resolve
137
+ }
138
+ let deleteBatch = (store, ids) => {
139
+ ids->Array.forEach(((id, subId)) =>
140
+ switch (subId, Spec.subIdConfig) {
141
+ | (None, _) => store->deleteStates(id)
142
+ | (Some((_, subId)), Some({Reventless.ReadModel.getSubId: getSubId})) =>
143
+ store->deleteSubState(id, subId, getSubId)
144
+ | _ => ()
145
+ }
146
+ )
147
+ Ok()->Promise.resolve
148
+ }
149
+
150
+ let runActions = (actions, operations) =>
151
+ actions->handleActions(operations, Spec.subIdConfig)
152
+
153
+ let sortStore = store =>
154
+ switch Spec.subIdConfig {
155
+ | None => store
156
+ | Some(_) =>
157
+ Dict.mapValues(store, states =>
158
+ states->Array.toSorted((s1, s2) =>
159
+ String.compare(s1->getSubId->Option.getUnsafe, s2->getSubId->Option.getUnsafe)
160
+ )
161
+ )
162
+ }
163
+
164
+ let update = async (store, events) => {
165
+ // Projection.project produces an array of actions per event — flatten and
166
+ // feed them through handleActions against the dict store.
167
+ let actions =
168
+ events
169
+ ->Array.map(ev => ev->Projection.project)
170
+ ->Array.flat
171
+ await actions->runActions({
172
+ load: load(store, ...),
173
+ loadStream: id => store->states(id)->Stream.fromIterable,
174
+ save: save(store, ...),
175
+ saveBatch: saveBatch(store, ...),
176
+ count: async (_, _, _) => Ok(0),
177
+ delete: delete(store, ...),
178
+ deleteBatch: deleteBatch(store, ...),
179
+ })
180
+
181
+ store->sortStore
182
+ }
183
+
184
+ let givenEvents = events => Dict.make()->update(events)
185
+
186
+ let whenEvent = (store, event): storeThunk =>
187
+ () => (async () => await (await store)->update([event]))()
188
+ let whenEvents = (store, events): storeThunk =>
189
+ () => (async () => await (await store)->update(events))()
190
+
191
+ let encState = (s: Spec.state) => s->Message.encode(Spec.stateSchema)
192
+ let encStates = (arr: array<Spec.state>) => arr->Array.map(encState)
193
+
194
+ let stateEq = (a: Spec.state, b: Spec.state) =>
195
+ JSON.stringify(encState(a)) == JSON.stringify(encState(b))
196
+ let statesEq = (a, b) =>
197
+ Array.length(a) == Array.length(b) &&
198
+ Array.zip(a, b)->Array.every(((x, y)) => stateEq(x, y))
199
+ let storeEq = (a: store, b: store) => {
200
+ let ka = a->Dict.keysToArray->Array.toSorted(String.compare)
201
+ let kb = b->Dict.keysToArray->Array.toSorted(String.compare)
202
+ ka == kb &&
203
+ ka->Array.every(key =>
204
+ statesEq(a->Dict.get(key)->Option.getOr([]), b->Dict.get(key)->Option.getOr([]))
205
+ )
206
+ }
207
+
208
+ let encStore = (s: store) => {
209
+ let obj = Dict.make()
210
+ s
211
+ ->Dict.toArray
212
+ ->Array.forEach(((k, v)) => obj->Dict.set(k, JSON.Encode.array(v->encStates)))
213
+ JSON.Encode.object(obj)
214
+ }
215
+
216
+ let thenStates = async (thunk, expectedStates) => {
217
+ let store = await thunk()
218
+ let keys = store->Dict.keysToArray
219
+ let actualId = keys->Array.get(0)
220
+ let actualStates = store->Dict.valuesToArray->Array.get(0)->Option.getOr([])
221
+ if (
222
+ keys->Array.length == 1 &&
223
+ actualId == Some(testId.contents) &&
224
+ statesEq(actualStates, expectedStates)
225
+ ) {
226
+ Outcome.pass
227
+ } else {
228
+ Outcome.fail(
229
+ StateMismatch({
230
+ key: testId.contents,
231
+ expected: Some(JSON.Encode.array(expectedStates->encStates)),
232
+ actual: Some(encStore(store)),
233
+ }),
234
+ )
235
+ }
236
+ }
237
+
238
+ let thenStatesWithId = async (thunk, id, expectedStates) => {
239
+ let store = await thunk()
240
+ let keys = store->Dict.keysToArray
241
+ let actualId = keys->Array.get(0)
242
+ let actualStates = store->Dict.valuesToArray->Array.get(0)->Option.getOr([])
243
+ if keys->Array.length == 1 && actualId == Some(id) && statesEq(actualStates, expectedStates) {
244
+ Outcome.pass
245
+ } else {
246
+ Outcome.fail(
247
+ StateMismatch({
248
+ key: id,
249
+ expected: Some(JSON.Encode.array(expectedStates->encStates)),
250
+ actual: Some(encStore(store)),
251
+ }),
252
+ )
253
+ }
254
+ }
255
+
256
+ let thenAllStates = async (thunk, expectedStore: store) => {
257
+ let store = await thunk()
258
+ if storeEq(store, expectedStore) {
259
+ Outcome.pass
260
+ } else {
261
+ Outcome.fail(
262
+ StateMismatch({
263
+ key: "<all>",
264
+ expected: Some(encStore(expectedStore)),
265
+ actual: Some(encStore(store)),
266
+ }),
267
+ )
268
+ }
269
+ }
270
+
271
+ let thenState = (thunk, expectedState) => thenStates(thunk, [expectedState])
272
+ let thenStateWithId = (thunk, id, expectedState) =>
273
+ thenStatesWithId(thunk, id, [expectedState])
274
+
275
+ let thenNoState = async thunk => {
276
+ let store = await thunk()
277
+ let total =
278
+ store->Dict.valuesToArray->Array.reduce(0, (acc, states) => acc + states->Array.length)
279
+ if total == 0 {
280
+ Outcome.pass
281
+ } else {
282
+ Outcome.fail(
283
+ StateMismatch({
284
+ key: "<any>",
285
+ expected: None,
286
+ actual: Some(encStore(store)),
287
+ }),
288
+ )
289
+ }
290
+ }
291
+
292
+ let thenThrow = async thunk =>
293
+ switch await thunk() {
294
+ | _ =>
295
+ Outcome.fail(
296
+ Throw({
297
+ error: "Expected thunk to throw but it returned normally",
298
+ stack: "",
299
+ }),
300
+ )
301
+ | exception _ => Outcome.pass
302
+ }
303
+
304
+ let thenFail = async thunk =>
305
+ switch await thunk() {
306
+ | _ =>
307
+ Outcome.fail(
308
+ Throw({error: "Expected failure but thunk returned normally", stack: ""}),
309
+ )
310
+ | exception _ => Outcome.pass
311
+ }
312
+ }