@reventlessdev/reventless-local 3.0.0-alpha.243 → 3.0.0-alpha.245

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.
@@ -0,0 +1,140 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodenet from "node:net";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as EventTapSocketFixturesMjs from "./EventTapSocketFixtures.mjs";
8
+ import * as LocalEventTap$ReventlessLocal from "../src/adapter/LocalEventTap.res.mjs";
9
+
10
+ function recordingSocket(prim) {
11
+ return EventTapSocketFixturesMjs.recordingSocket();
12
+ }
13
+
14
+ function throwingSocket(prim) {
15
+ return EventTapSocketFixturesMjs.throwingSocket();
16
+ }
17
+
18
+ function envWith(value) {
19
+ if (value !== undefined) {
20
+ return Object.fromEntries([[
21
+ LocalEventTap$ReventlessLocal.envVar,
22
+ value
23
+ ]]);
24
+ } else {
25
+ return {};
26
+ }
27
+ }
28
+
29
+ globalThis.describe("LocalEventTap.settingFromEnv", () => {
30
+ globalThis.test("defaults to an ephemeral socket when the var is unset", () => {
31
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith(undefined))).toEqual("Ephemeral");
32
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith(""))).toEqual("Ephemeral");
33
+ });
34
+ globalThis.test("takes a port when the value is one", () => {
35
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith("4100"))).toEqual({
36
+ TAG: "Fixed",
37
+ _0: 4100
38
+ });
39
+ });
40
+ globalThis.test("treats a non-port value as the default, not as a port", () => {
41
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith("ndjson"))).toEqual("Ephemeral");
42
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith("1"))).toEqual("Ephemeral");
43
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith("80"))).toEqual("Ephemeral");
44
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith("70000"))).toEqual("Ephemeral");
45
+ });
46
+ globalThis.test("switches the socket off on request", () => {
47
+ [
48
+ "off",
49
+ "false",
50
+ "none"
51
+ ].forEach(v => {
52
+ globalThis.expect(LocalEventTap$ReventlessLocal.settingFromEnv(envWith(v))).toEqual("Off");
53
+ });
54
+ });
55
+ });
56
+
57
+ globalThis.describe("LocalEventTap.stdoutEnabled", () => {
58
+ globalThis.test("stays off unless the var is set", () => {
59
+ globalThis.expect(LocalEventTap$ReventlessLocal.stdoutEnabled(envWith(undefined))).toEqual(false);
60
+ globalThis.expect(LocalEventTap$ReventlessLocal.stdoutEnabled(envWith("ndjson"))).toEqual(true);
61
+ globalThis.expect(LocalEventTap$ReventlessLocal.stdoutEnabled(envWith("4100"))).toEqual(true);
62
+ });
63
+ globalThis.test("is silenced by off, along with the socket", () => {
64
+ globalThis.expect(LocalEventTap$ReventlessLocal.stdoutEnabled(envWith("off"))).toEqual(false);
65
+ });
66
+ });
67
+
68
+ globalThis.describe("LocalEventTap.broadcast", () => {
69
+ globalThis.test("hands every reader the line, newline-terminated", () => {
70
+ LocalEventTap$ReventlessLocal.resetForTests();
71
+ let a = EventTapSocketFixturesMjs.recordingSocket();
72
+ let b = EventTapSocketFixturesMjs.recordingSocket();
73
+ LocalEventTap$ReventlessLocal.addConnectionForTests(a.socket);
74
+ LocalEventTap$ReventlessLocal.addConnectionForTests(b.socket);
75
+ LocalEventTap$ReventlessLocal.broadcast(`@@RVLESS_EVT@@ {"seq":1}`);
76
+ globalThis.expect(a.written).toEqual([`@@RVLESS_EVT@@ {"seq":1}\n`]);
77
+ globalThis.expect(b.written).toEqual([`@@RVLESS_EVT@@ {"seq":1}\n`]);
78
+ });
79
+ globalThis.test("drops a reader whose write throws, and keeps serving the rest", () => {
80
+ LocalEventTap$ReventlessLocal.resetForTests();
81
+ let live = EventTapSocketFixturesMjs.recordingSocket();
82
+ LocalEventTap$ReventlessLocal.addConnectionForTests(EventTapSocketFixturesMjs.throwingSocket());
83
+ LocalEventTap$ReventlessLocal.addConnectionForTests(live.socket);
84
+ LocalEventTap$ReventlessLocal.broadcast("first");
85
+ LocalEventTap$ReventlessLocal.broadcast("second");
86
+ globalThis.expect(live.written).toEqual([
87
+ "first\n",
88
+ "second\n"
89
+ ]);
90
+ });
91
+ globalThis.test("is inert with no readers", () => {
92
+ LocalEventTap$ReventlessLocal.resetForTests();
93
+ LocalEventTap$ReventlessLocal.broadcast("nobody is listening");
94
+ globalThis.expect(LocalEventTap$ReventlessLocal.port()).toEqual(undefined);
95
+ });
96
+ });
97
+
98
+ globalThis.describe("LocalEventTap.start", () => {
99
+ globalThis.test("does not listen when the socket is switched off", () => {
100
+ LocalEventTap$ReventlessLocal.resetForTests();
101
+ LocalEventTap$ReventlessLocal.start(envWith("off"), undefined, undefined);
102
+ globalThis.expect(LocalEventTap$ReventlessLocal.port()).toEqual(undefined);
103
+ });
104
+ globalThis.test("binds an ephemeral port by default and reports the one it got", async () => {
105
+ LocalEventTap$ReventlessLocal.resetForTests();
106
+ let bound = await new Promise((resolve, _reject) => LocalEventTap$ReventlessLocal.start(envWith(undefined), p => resolve(p), undefined));
107
+ globalThis.expect(bound > 0).toEqual(true);
108
+ globalThis.expect(LocalEventTap$ReventlessLocal.port()).toEqual(bound);
109
+ return await LocalEventTap$ReventlessLocal.stopForTests();
110
+ });
111
+ globalThis.test("serves the lines it is sent on the port it was named", async () => {
112
+ LocalEventTap$ReventlessLocal.resetForTests();
113
+ let bound = await new Promise((resolve, _reject) => LocalEventTap$ReventlessLocal.start(envWith((47311).toString()), p => resolve(p), undefined));
114
+ globalThis.expect(bound).toEqual(47311);
115
+ let received = await new Promise((resolve, _reject) => {
116
+ let socket = Nodenet.createConnection(47311, "127.0.0.1", () => {});
117
+ socket.setEncoding("utf8");
118
+ socket.on("error", e => resolve("connect failed: " + Stdlib_Option.getOr(Stdlib_JsExn.message(e), "unknown")));
119
+ let ticker = {
120
+ contents: undefined
121
+ };
122
+ socket.on("data", chunk => {
123
+ Stdlib_Option.forEach(ticker.contents, prim => {
124
+ clearInterval(prim);
125
+ });
126
+ resolve(chunk);
127
+ });
128
+ ticker.contents = Primitive_option.some(setInterval(() => LocalEventTap$ReventlessLocal.broadcast(`@@RVLESS_EVT@@ {"seq":7}`), 5));
129
+ });
130
+ globalThis.expect(received).toEqual(`@@RVLESS_EVT@@ {"seq":7}\n`);
131
+ return await LocalEventTap$ReventlessLocal.stopForTests();
132
+ });
133
+ });
134
+
135
+ export {
136
+ recordingSocket,
137
+ throwingSocket,
138
+ envWith,
139
+ }
140
+ /* Not a pure module */
@@ -141,3 +141,64 @@ describe("LocalSeedTarget.storePath", () => {
141
141
  )->toEqual(true)
142
142
  })
143
143
  })
144
+
145
+ describe("LocalPlatformRegistry.tapPort", () => {
146
+ // Optional on purpose: `list` DELETES an entry it cannot decode, so a required
147
+ // field would make every platform from an older build vanish from the seed
148
+ // tools rather than fail loudly.
149
+ testSync("reads an entry written before the field existed", () => {
150
+ let cwd = tempRoot()
151
+ let dir = LocalPlatformRegistry.runningDir(~cwd, ())
152
+ NodeFs.mkdirSync(dir, {recursive: true})
153
+ let path = NodePath.join([dir, "4000.json"])
154
+ NodeFs.writeFileSync(
155
+ path,
156
+ `{"app":"old","port":4000,"pid":${NodeProcess.pid->Int.toString},"endpoint":"http://localhost:4000/graphql","loginEndpoint":"http://localhost:4000/__inmemory/login","store":{"kind":"memory"},"startedAt":"2026-08-14T00:00:00.000Z"}`,
157
+ )
158
+
159
+ switch LocalPlatformRegistry.list(~cwd, ())->Array.get(0) {
160
+ | Some(entry) => expect(entry.tapPort)->toEqual(None)
161
+ | None => fail("an entry without tapPort must still be listed")
162
+ }
163
+ expect(NodeFs.existsSync(path))->toEqual(true)
164
+ })
165
+
166
+ // The socket binds asynchronously, so the entry is published first and the port
167
+ // filled in on the listen callback — which is what keeps it from ever naming a
168
+ // port that is not listening.
169
+ testSync("fills the port in on an entry already written", () => {
170
+ let cwd = tempRoot()
171
+ let _ = writeAt(~cwd, ~port=4000, ~store=memoryStore)
172
+ expect(LocalPlatformRegistry.list(~cwd, ())->Array.map(e => e.tapPort))->toEqual([None])
173
+
174
+ LocalPlatformRegistry.publishTapPort(~port=4000, ~tapPort=58909, ~cwd)
175
+
176
+ switch LocalPlatformRegistry.list(~cwd, ())->Array.get(0) {
177
+ | Some(entry) =>
178
+ expect(entry.tapPort)->toEqual(Some(58909))
179
+ // The rest of the entry survives the rewrite — a reader still finds the
180
+ // endpoint and store it came for.
181
+ expect(entry.endpoint)->toEqual("http://localhost:4000/graphql")
182
+ expect(entry.store.kind)->toEqual("memory")
183
+ | None => fail("the updated entry must still be listed")
184
+ }
185
+ })
186
+
187
+ testSync("ignores a platform that never published an entry", () =>
188
+ LocalPlatformRegistry.publishTapPort(~port=4000, ~tapPort=1, ~cwd=tempRoot())
189
+ )
190
+
191
+ testSync("round-trips the port a platform serving a tap publishes", () => {
192
+ let cwd = tempRoot()
193
+ let _ = LocalPlatformRegistry.write(
194
+ ~port=4000,
195
+ ~endpoint="http://localhost:4000/graphql",
196
+ ~loginEndpoint="http://localhost:4000/__inmemory/login",
197
+ ~store=memoryStore,
198
+ ~tapPort=4100,
199
+ ~cwd,
200
+ )
201
+
202
+ expect(LocalPlatformRegistry.list(~cwd, ())->Array.map(e => e.tapPort))->toEqual([Some(4100)])
203
+ })
204
+ })
@@ -25,7 +25,7 @@ let memoryStore = {
25
25
 
26
26
  function writeAt(cwd, port, store, pidOpt) {
27
27
  let pid = pidOpt !== undefined ? pidOpt : process.pid;
28
- return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, pid, cwd);
28
+ return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
29
29
  }
30
30
 
31
31
  globalThis.describe("LocalPlatformRegistry", () => {
@@ -140,6 +140,46 @@ globalThis.describe("LocalSeedTarget.storePath", () => {
140
140
  });
141
141
  });
142
142
 
143
+ globalThis.describe("LocalPlatformRegistry.tapPort", () => {
144
+ globalThis.test("reads an entry written before the field existed", () => {
145
+ let cwd = tempRoot();
146
+ let dir = LocalPlatformRegistry$ReventlessLocal.runningDir(cwd, undefined);
147
+ Nodefs.mkdirSync(dir, {
148
+ recursive: true
149
+ });
150
+ let path = Nodepath.join(dir, "4000.json");
151
+ Nodefs.writeFileSync(path, `{"app":"old","port":4000,"pid":` + process.pid.toString() + `,"endpoint":"http://localhost:4000/graphql","loginEndpoint":"http://localhost:4000/__inmemory/login","store":{"kind":"memory"},"startedAt":"2026-08-14T00:00:00.000Z"}`, "utf8");
152
+ let entry = LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined)[0];
153
+ if (entry !== undefined) {
154
+ globalThis.expect(entry.tapPort).toEqual(undefined);
155
+ } else {
156
+ JestGlobals.fail("an entry without tapPort must still be listed");
157
+ }
158
+ globalThis.expect(Nodefs.existsSync(path)).toEqual(true);
159
+ });
160
+ globalThis.test("fills the port in on an entry already written", () => {
161
+ let cwd = tempRoot();
162
+ writeAt(cwd, 4000, memoryStore, undefined);
163
+ globalThis.expect(LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).map(e => e.tapPort)).toEqual([undefined]);
164
+ LocalPlatformRegistry$ReventlessLocal.publishTapPort(4000, 58909, cwd);
165
+ let entry = LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined)[0];
166
+ if (entry !== undefined) {
167
+ globalThis.expect(entry.tapPort).toEqual(58909);
168
+ globalThis.expect(entry.endpoint).toEqual("http://localhost:4000/graphql");
169
+ globalThis.expect(entry.store.kind).toEqual("memory");
170
+ return;
171
+ } else {
172
+ return JestGlobals.fail("the updated entry must still be listed");
173
+ }
174
+ });
175
+ globalThis.test("ignores a platform that never published an entry", () => LocalPlatformRegistry$ReventlessLocal.publishTapPort(4000, 1, tempRoot()));
176
+ globalThis.test("round-trips the port a platform serving a tap publishes", () => {
177
+ let cwd = tempRoot();
178
+ LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, 4100, undefined, cwd);
179
+ globalThis.expect(LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).map(e => e.tapPort)).toEqual([4100]);
180
+ });
181
+ });
182
+
143
183
  let deadPid = 2147483646;
144
184
 
145
185
  export {
@@ -0,0 +1,176 @@
1
+ // A reset used to unlink a served store at construction and only discover at the
2
+ // bind, ~1200 lines later, that it had lost the race.
3
+
4
+ @@warning("-44")
5
+
6
+ open JestGlobals
7
+
8
+ let tempRoot = (): string =>
9
+ NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-start-"]))
10
+
11
+ let deadPid = 2147483646
12
+
13
+ let sqliteStore = (path): LocalPlatformRegistry.store => {kind: "sqlite", path: Some(path)}
14
+ let memoryStore: LocalPlatformRegistry.store = {kind: "memory", path: None}
15
+
16
+ let writeAt = (~cwd, ~port, ~store, ~pid=NodeProcess.pid) =>
17
+ LocalPlatformRegistry.write(
18
+ ~port,
19
+ ~endpoint=`http://localhost:${port->Int.toString}/graphql`,
20
+ ~loginEndpoint=`http://localhost:${port->Int.toString}/__inmemory/login`,
21
+ ~store,
22
+ ~pid,
23
+ ~cwd,
24
+ )
25
+
26
+ let withoutBypass = (f: unit => unit) => {
27
+ let previous = NodeProcess.env->Dict.get(LocalPlatformStart.bypassEnv)
28
+ NodeProcess.env->Dict.delete(LocalPlatformStart.bypassEnv)
29
+ let restore = () =>
30
+ switch previous {
31
+ | Some(v) => NodeProcess.env->Dict.set(LocalPlatformStart.bypassEnv, v)
32
+ | None => ()
33
+ }
34
+ try {
35
+ f()
36
+ restore()
37
+ } catch {
38
+ | e =>
39
+ restore()
40
+ throw(e)
41
+ }
42
+ }
43
+
44
+ describe("LocalPlatformStart.guardReset", () => {
45
+ // The exact sequence that lost data.
46
+ testSync("refuses a reset of a store a live platform opened, naming it", () => {
47
+ let cwd = tempRoot()
48
+ let store = NodePath.join([cwd, ".reventless", "local.db"])
49
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(store))
50
+
51
+ switch LocalPlatformStart.guardReset(~path=store, ~cwd, ()) {
52
+ | () => fail("expected the reset to be refused")
53
+ | exception JsExn(e) =>
54
+ let message = e->JsExn.message->Option.getOr("")
55
+ expect(message->String.includes(":4000"))->toEqual(true)
56
+ expect(message->String.includes(NodeProcess.pid->Int.toString))->toEqual(true)
57
+ }
58
+ })
59
+
60
+ // Entry paths are absolute; the configured one is what an operator typed.
61
+ testSync("matches a relative configured path against the absolute registered one", () => {
62
+ let cwd = tempRoot()
63
+ let absolute = NodePath.join([NodeProcess.cwd(), ".reventless", "guard-probe.db"])
64
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(absolute))
65
+
66
+ expect(
67
+ LocalPlatformStart.servedBy(~path="./.reventless/guard-probe.db", ~cwd, ())->Option.isSome,
68
+ )->toEqual(true)
69
+ })
70
+
71
+ testSync("lets a reset through when nothing is serving that file", () => {
72
+ let cwd = tempRoot()
73
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(NodePath.join([cwd, "other.db"])))
74
+
75
+ expect(LocalPlatformStart.servedBy(~path=NodePath.join([cwd, "local.db"]), ~cwd, ()))->toEqual(
76
+ None,
77
+ )
78
+ LocalPlatformStart.guardReset(~path=NodePath.join([cwd, "local.db"]), ~cwd, ())
79
+ })
80
+
81
+ // A `kill -9` leaves the entry behind; a guard that trusted it would never pass.
82
+ testSync("lets a reset through when the platform holding the store is gone", () => {
83
+ let cwd = tempRoot()
84
+ let store = NodePath.join([cwd, "local.db"])
85
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(store), ~pid=deadPid)
86
+
87
+ LocalPlatformStart.guardReset(~path=store, ~cwd, ())
88
+ })
89
+
90
+ // An in-memory platform names no path, so it can never hold a store file.
91
+ testSync("ignores a platform that keeps its store in memory", () => {
92
+ let cwd = tempRoot()
93
+ let _ = writeAt(~cwd, ~port=4010, ~store=memoryStore)
94
+
95
+ LocalPlatformStart.guardReset(~path=NodePath.join([cwd, "local.db"]), ~cwd, ())
96
+ })
97
+ })
98
+
99
+ describe("LocalPlatformStart.orAddressRunning", () => {
100
+ // The addressing path exits 0, so running it first would report success for a
101
+ // wipe that never happened.
102
+ testSync("refuses a reset before it addresses the platform holding the store", () => {
103
+ let cwd = tempRoot()
104
+ let relative = "./.reventless/order-probe.db"
105
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(NodePath.resolve([relative])))
106
+
107
+ let previousBackend = NodeProcess.env->Dict.get("REVENTLESS_LOCAL_BACKEND")
108
+ NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", `sqlite:${relative}?reset`)
109
+ let outcome = switch LocalPlatformStart.orAddressRunning(~cwd, ()) {
110
+ | () => "returned or exited"
111
+ | exception JsExn(e) => e->JsExn.message->Option.getOr("")
112
+ }
113
+ switch previousBackend {
114
+ | Some(v) => NodeProcess.env->Dict.set("REVENTLESS_LOCAL_BACKEND", v)
115
+ | None => NodeProcess.env->Dict.delete("REVENTLESS_LOCAL_BACKEND")
116
+ }
117
+
118
+ expect(outcome->String.includes("refusing to reset"))->toEqual(true)
119
+ expect(outcome->String.includes(":4000"))->toEqual(true)
120
+ })
121
+ })
122
+
123
+ describe("LocalPlatformStart.decide", () => {
124
+ testSync("starts where nothing is running", () =>
125
+ withoutBypass(() =>
126
+ expect(LocalPlatformStart.decide(~cwd=tempRoot(), ()))->toEqual(LocalPlatformStart.Start)
127
+ )
128
+ )
129
+
130
+ testSync("addresses the one platform already serving this directory", () =>
131
+ withoutBypass(() => {
132
+ let cwd = tempRoot()
133
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(NodePath.join([cwd, "local.db"])))
134
+
135
+ switch LocalPlatformStart.decide(~cwd, ()) {
136
+ | Start => fail("expected the running platform to be addressed")
137
+ | AlreadyRunning(entries) =>
138
+ expect(entries->Array.map(e => e.port))->toEqual([4000])
139
+ let lines = LocalPlatformStart.report(entries)
140
+ expect(lines->Array.length)->toEqual(2)
141
+ expect(lines->Array.join("\n")->String.includes(":4000"))->toEqual(true)
142
+ }
143
+ })
144
+ )
145
+
146
+ // The state this is trying to make impossible: say so rather than pick one.
147
+ testSync("names all of them when two are running", () =>
148
+ withoutBypass(() => {
149
+ let cwd = tempRoot()
150
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(NodePath.join([cwd, "local.db"])))
151
+ let _ = writeAt(~cwd, ~port=4010, ~store=memoryStore)
152
+
153
+ switch LocalPlatformStart.decide(~cwd, ()) {
154
+ | Start => fail("expected both running platforms to be reported")
155
+ | AlreadyRunning(entries) =>
156
+ let printed = LocalPlatformStart.report(entries)->Array.join("\n")
157
+ expect(printed->String.includes(":4000"))->toEqual(true)
158
+ expect(printed->String.includes(":4010"))->toEqual(true)
159
+ }
160
+ })
161
+ )
162
+
163
+ // The escape hatch the e2e suites and the VS Code runner already take.
164
+ testSync("starts anyway when REVENTLESS_DOMAIN_PORT names a port", () => {
165
+ let cwd = tempRoot()
166
+ let _ = writeAt(~cwd, ~port=4000, ~store=sqliteStore(NodePath.join([cwd, "local.db"])))
167
+ let previous = NodeProcess.env->Dict.get(LocalPlatformStart.bypassEnv)
168
+ NodeProcess.env->Dict.set(LocalPlatformStart.bypassEnv, "4010")
169
+ let decision = LocalPlatformStart.decide(~cwd, ())
170
+ switch previous {
171
+ | Some(v) => NodeProcess.env->Dict.set(LocalPlatformStart.bypassEnv, v)
172
+ | None => NodeProcess.env->Dict.delete(LocalPlatformStart.bypassEnv)
173
+ }
174
+ expect(decision)->toEqual(LocalPlatformStart.Start)
175
+ })
176
+ })
@@ -0,0 +1,210 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodeos from "node:os";
5
+ import * as Nodepath from "node:path";
6
+ import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
7
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
8
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+ import * as LocalPlatformStart$ReventlessLocal from "../src/LocalPlatformStart.res.mjs";
12
+ import * as LocalPlatformRegistry$ReventlessLocal from "../src/LocalPlatformRegistry.res.mjs";
13
+
14
+ function tempRoot() {
15
+ return Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-start-"));
16
+ }
17
+
18
+ function sqliteStore(path) {
19
+ return {
20
+ kind: "sqlite",
21
+ path: path
22
+ };
23
+ }
24
+
25
+ let memoryStore = {
26
+ kind: "memory",
27
+ path: undefined
28
+ };
29
+
30
+ function writeAt(cwd, port, store, pidOpt) {
31
+ let pid = pidOpt !== undefined ? pidOpt : process.pid;
32
+ return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
33
+ }
34
+
35
+ function withoutBypass(f) {
36
+ let previous = process.env[LocalPlatformStart$ReventlessLocal.bypassEnv];
37
+ Stdlib_Dict.$$delete(process.env, LocalPlatformStart$ReventlessLocal.bypassEnv);
38
+ let restore = () => {
39
+ if (previous !== undefined) {
40
+ process.env[LocalPlatformStart$ReventlessLocal.bypassEnv] = previous;
41
+ return;
42
+ }
43
+ };
44
+ try {
45
+ f();
46
+ return restore();
47
+ } catch (e) {
48
+ restore();
49
+ throw e;
50
+ }
51
+ }
52
+
53
+ globalThis.describe("LocalPlatformStart.guardReset", () => {
54
+ globalThis.test("refuses a reset of a store a live platform opened, naming it", () => {
55
+ let cwd = tempRoot();
56
+ let store = Nodepath.join(cwd, ".reventless", "local.db");
57
+ writeAt(cwd, 4000, {
58
+ kind: "sqlite",
59
+ path: store
60
+ }, undefined);
61
+ let val;
62
+ try {
63
+ val = LocalPlatformStart$ReventlessLocal.guardReset(store, cwd, undefined);
64
+ } catch (raw_e) {
65
+ let e = Primitive_exceptions.internalToException(raw_e);
66
+ if (e.RE_EXN_ID === "JsExn") {
67
+ let message = Stdlib_Option.getOr(Stdlib_JsExn.message(e._1), "");
68
+ globalThis.expect(message.includes(":4000")).toEqual(true);
69
+ globalThis.expect(message.includes(process.pid.toString())).toEqual(true);
70
+ return;
71
+ }
72
+ throw e;
73
+ }
74
+ JestGlobals.fail("expected the reset to be refused");
75
+ });
76
+ globalThis.test("matches a relative configured path against the absolute registered one", () => {
77
+ let cwd = tempRoot();
78
+ let absolute = Nodepath.join(process.cwd(), ".reventless", "guard-probe.db");
79
+ writeAt(cwd, 4000, {
80
+ kind: "sqlite",
81
+ path: absolute
82
+ }, undefined);
83
+ globalThis.expect(Stdlib_Option.isSome(LocalPlatformStart$ReventlessLocal.servedBy("./.reventless/guard-probe.db", cwd, undefined))).toEqual(true);
84
+ });
85
+ globalThis.test("lets a reset through when nothing is serving that file", () => {
86
+ let cwd = tempRoot();
87
+ let path = Nodepath.join(cwd, "other.db");
88
+ writeAt(cwd, 4000, {
89
+ kind: "sqlite",
90
+ path: path
91
+ }, undefined);
92
+ globalThis.expect(LocalPlatformStart$ReventlessLocal.servedBy(Nodepath.join(cwd, "local.db"), cwd, undefined)).toEqual(undefined);
93
+ LocalPlatformStart$ReventlessLocal.guardReset(Nodepath.join(cwd, "local.db"), cwd, undefined);
94
+ });
95
+ globalThis.test("lets a reset through when the platform holding the store is gone", () => {
96
+ let cwd = tempRoot();
97
+ let store = Nodepath.join(cwd, "local.db");
98
+ writeAt(cwd, 4000, {
99
+ kind: "sqlite",
100
+ path: store
101
+ }, 2147483646);
102
+ LocalPlatformStart$ReventlessLocal.guardReset(store, cwd, undefined);
103
+ });
104
+ globalThis.test("ignores a platform that keeps its store in memory", () => {
105
+ let cwd = tempRoot();
106
+ writeAt(cwd, 4010, memoryStore, undefined);
107
+ LocalPlatformStart$ReventlessLocal.guardReset(Nodepath.join(cwd, "local.db"), cwd, undefined);
108
+ });
109
+ });
110
+
111
+ globalThis.describe("LocalPlatformStart.orAddressRunning", () => {
112
+ globalThis.test("refuses a reset before it addresses the platform holding the store", () => {
113
+ let cwd = tempRoot();
114
+ let relative = "./.reventless/order-probe.db";
115
+ let path = Nodepath.resolve(relative);
116
+ writeAt(cwd, 4000, {
117
+ kind: "sqlite",
118
+ path: path
119
+ }, undefined);
120
+ let previousBackend = process.env["REVENTLESS_LOCAL_BACKEND"];
121
+ process.env["REVENTLESS_LOCAL_BACKEND"] = `sqlite:` + relative + `?reset`;
122
+ let outcome;
123
+ try {
124
+ LocalPlatformStart$ReventlessLocal.orAddressRunning(cwd, undefined);
125
+ outcome = "returned or exited";
126
+ } catch (raw_e) {
127
+ let e = Primitive_exceptions.internalToException(raw_e);
128
+ if (e.RE_EXN_ID === "JsExn") {
129
+ outcome = Stdlib_Option.getOr(Stdlib_JsExn.message(e._1), "");
130
+ } else {
131
+ throw e;
132
+ }
133
+ }
134
+ if (previousBackend !== undefined) {
135
+ process.env["REVENTLESS_LOCAL_BACKEND"] = previousBackend;
136
+ } else {
137
+ Stdlib_Dict.$$delete(process.env, "REVENTLESS_LOCAL_BACKEND");
138
+ }
139
+ globalThis.expect(outcome.includes("refusing to reset")).toEqual(true);
140
+ globalThis.expect(outcome.includes(":4000")).toEqual(true);
141
+ });
142
+ });
143
+
144
+ globalThis.describe("LocalPlatformStart.decide", () => {
145
+ globalThis.test("starts where nothing is running", () => withoutBypass(() => {
146
+ globalThis.expect(LocalPlatformStart$ReventlessLocal.decide(tempRoot(), undefined)).toEqual("Start");
147
+ }));
148
+ globalThis.test("addresses the one platform already serving this directory", () => withoutBypass(() => {
149
+ let cwd = tempRoot();
150
+ let path = Nodepath.join(cwd, "local.db");
151
+ writeAt(cwd, 4000, {
152
+ kind: "sqlite",
153
+ path: path
154
+ }, undefined);
155
+ let entries = LocalPlatformStart$ReventlessLocal.decide(cwd, undefined);
156
+ if (typeof entries !== "object") {
157
+ return JestGlobals.fail("expected the running platform to be addressed");
158
+ }
159
+ let entries$1 = entries._0;
160
+ globalThis.expect(entries$1.map(e => e.port)).toEqual([4000]);
161
+ let lines = LocalPlatformStart$ReventlessLocal.report(entries$1);
162
+ globalThis.expect(lines.length).toEqual(2);
163
+ globalThis.expect(lines.join("\n").includes(":4000")).toEqual(true);
164
+ }));
165
+ globalThis.test("names all of them when two are running", () => withoutBypass(() => {
166
+ let cwd = tempRoot();
167
+ let path = Nodepath.join(cwd, "local.db");
168
+ writeAt(cwd, 4000, {
169
+ kind: "sqlite",
170
+ path: path
171
+ }, undefined);
172
+ writeAt(cwd, 4010, memoryStore, undefined);
173
+ let entries = LocalPlatformStart$ReventlessLocal.decide(cwd, undefined);
174
+ if (typeof entries !== "object") {
175
+ return JestGlobals.fail("expected both running platforms to be reported");
176
+ }
177
+ let printed = LocalPlatformStart$ReventlessLocal.report(entries._0).join("\n");
178
+ globalThis.expect(printed.includes(":4000")).toEqual(true);
179
+ globalThis.expect(printed.includes(":4010")).toEqual(true);
180
+ }));
181
+ globalThis.test("starts anyway when REVENTLESS_DOMAIN_PORT names a port", () => {
182
+ let cwd = tempRoot();
183
+ let path = Nodepath.join(cwd, "local.db");
184
+ writeAt(cwd, 4000, {
185
+ kind: "sqlite",
186
+ path: path
187
+ }, undefined);
188
+ let previous = process.env[LocalPlatformStart$ReventlessLocal.bypassEnv];
189
+ process.env[LocalPlatformStart$ReventlessLocal.bypassEnv] = "4010";
190
+ let decision = LocalPlatformStart$ReventlessLocal.decide(cwd, undefined);
191
+ if (previous !== undefined) {
192
+ process.env[LocalPlatformStart$ReventlessLocal.bypassEnv] = previous;
193
+ } else {
194
+ Stdlib_Dict.$$delete(process.env, LocalPlatformStart$ReventlessLocal.bypassEnv);
195
+ }
196
+ globalThis.expect(decision).toEqual("Start");
197
+ });
198
+ });
199
+
200
+ let deadPid = 2147483646;
201
+
202
+ export {
203
+ tempRoot,
204
+ deadPid,
205
+ sqliteStore,
206
+ memoryStore,
207
+ writeAt,
208
+ withoutBypass,
209
+ }
210
+ /* Not a pure module */
@@ -1,8 +1,7 @@
1
- // Tests for the opt-in NDJSON domain-event tap (features plan Phase 9 — the VS
2
- // Code local platform runner). The tap lives in LocalBus.publishEvent: when
3
- // REVENTLESS_EVENT_TAP is set it emits one sentinel-prefixed JSON line per
4
- // published event to stdout (console.log), with the event's real topic name and
5
- // payload. Off by default so normal runs stay quiet.
1
+ // The tap's stdout sink, which is the opt-in half: REVENTLESS_EVENT_TAP puts one
2
+ // sentinel-prefixed JSON line per published event on stdout, and stays off by
3
+ // default so normal runs are quiet. The socket sink is default-on and covered by
4
+ // LocalEventTapTest.
6
5
 
7
6
  open JestGlobals
8
7
 
@@ -84,8 +83,19 @@ describe("LocalBus event tap (Phase 9)", () => {
84
83
  expect(obj->Dict.get("topic")->Option.flatMap(JSON.Decode.string))->toEqual(
85
84
  Some("CatalogEventTopic"),
86
85
  )
87
- expect(obj->Dict.get("seq")->Option.flatMap(JSON.Decode.float))->toEqual(Some(1.0))
88
86
  expect(obj->Dict.get("payload")->Option.isSome)->toBe(true)
87
+
88
+ // Consecutive, not absolute: `seq` is the event's ordinal in the store (it is
89
+ // seeded from the persisted count at startup and counts every publish, tapped
90
+ // or not), so a reader that connects late still sees the real event number.
91
+ // The counter is module-level, so an absolute assertion here would depend on
92
+ // what the rest of the file published first.
93
+ let seqOf = line => {
94
+ let j = line->String.slice(~start=sentinel->String.length, ~end=line->String.length)
95
+ j->JSON.parseOrThrow->JSON.Decode.object->Option.getOrThrow->Dict.get("seq")
96
+ ->Option.flatMap(JSON.Decode.float)->Option.getOr(0.)
97
+ }
98
+ expect(seqOf(lines->Array.getUnsafe(1)) -. seqOf(first))->toEqual(1.)
89
99
  })
90
100
 
91
101
  testPromise("tap emits even when the topic has no subscribers", async () => {