@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.
- package/CHANGELOG.md +916 -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 +569 -0
- package/src/Behavior_GWT.res.mjs +523 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +147 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +37 -0
- package/src/Cancellation.res.mjs +46 -0
- package/src/ChildProcess.res +102 -0
- package/src/ChildProcess.res.mjs +107 -0
- package/src/Cli.res +905 -0
- package/src/Cli.res.mjs +952 -0
- package/src/Collector.res +212 -0
- package/src/Collector.res.mjs +218 -0
- package/src/ComponentMeta.res +77 -0
- package/src/ComponentMeta.res.mjs +91 -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 +99 -0
- package/src/DomainDeadCode.res +98 -0
- package/src/DomainDeadCode.res.mjs +72 -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 +45 -0
- package/src/Filter.res.mjs +47 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +109 -0
- package/src/FormatterHuman.res.mjs +115 -0
- package/src/FormatterJson.res +264 -0
- package/src/FormatterJson.res.mjs +322 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +100 -0
- package/src/FormatterTap.res.mjs +131 -0
- package/src/FormatterVsCode.res +352 -0
- package/src/FormatterVsCode.res.mjs +546 -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 +119 -0
- package/src/LocalHost.res.mjs +99 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -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 +97 -0
- package/src/Outcome.res.mjs +93 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +181 -0
- package/src/PlatformRunner.res.mjs +176 -0
- package/src/PlatformScan.res +142 -0
- package/src/PlatformScan.res.mjs +132 -0
- package/src/ProcessManager.res +135 -0
- package/src/ProcessManager.res.mjs +119 -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/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -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 +54 -0
- package/src/StubRuntime.res.mjs +55 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +108 -0
- package/src/Watch.res.mjs +85 -0
- package/src/WatcherProbe.res +56 -0
- package/src/WatcherProbe.res.mjs +59 -0
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +133 -0
- package/tests/BuildClassifierTest.res.mjs +176 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +67 -0
- package/tests/ComponentMetaTest.res.mjs +71 -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 +119 -0
- package/tests/DomainDeadCodeTest.res.mjs +158 -0
- package/tests/DomainGraphTest.res +448 -0
- package/tests/DomainGraphTest.res.mjs +505 -0
- package/tests/FlowAggregateGwtTest.res +171 -0
- package/tests/FlowAggregateGwtTest.res.mjs +273 -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 +433 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -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 +99 -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 +350 -0
- package/tests/MappingGwtTest.res.mjs +596 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +139 -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 +106 -0
- 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
|
+
}
|