@reventlessdev/reventless-local 3.0.0-alpha.244 → 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.
- package/CHANGELOG.md +11 -0
- package/package.json +10 -10
- package/src/LocalPlatformRegistry.res +54 -63
- package/src/LocalPlatformRegistry.res.mjs +24 -5
- package/src/LocalPlatformStart.res +84 -0
- package/src/LocalPlatformStart.res.mjs +99 -0
- package/src/Platform.res +16 -0
- package/src/Platform.res.mjs +11 -2
- package/src/adapter/LocalBus.res +30 -27
- package/src/adapter/LocalBus.res.mjs +14 -18
- package/src/adapter/LocalEventTap.res +139 -0
- package/src/adapter/LocalEventTap.res.mjs +177 -0
- package/tests/EventTapSocketFixtures.mjs +17 -0
- package/tests/LocalEventTapTest.res +154 -0
- package/tests/LocalEventTapTest.res.mjs +140 -0
- package/tests/LocalPlatformRegistryTest.res +61 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +41 -1
- package/tests/LocalPlatformStartTest.res +176 -0
- package/tests/LocalPlatformStartTest.res.mjs +210 -0
- package/tests/adapter/EventTapTest.res +16 -6
- package/tests/adapter/EventTapTest.res.mjs +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.245 (2026-09-04)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **logging:** let a runtime declare whether a person reads its logs ([df7a6cb](https://github.com/ReventlessDev/reventless-core/commit/df7a6cbdff62b856ea6b9d1f169b913563ba13a1))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **local:** one platform per app directory, and a reset that refuses a served store ([e0fd219](https://github.com/ReventlessDev/reventless-core/commit/e0fd219a8c1bbda85589374ffa75aaab0f3e4fda))
|
|
14
|
+
* **local:** serve the event tap over a socket, on by default ([1babf96](https://github.com/ReventlessDev/reventless-core/commit/1babf960f032805418e3ac8f25d03ceb3dcd8d20))
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# 3.0.0-alpha.244 (2026-09-04)
|
|
7
18
|
|
|
8
19
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.245",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"sury": "11.0.0-rc.2",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
39
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
39
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
40
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.9",
|
|
40
41
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
41
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
49
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.104",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.203",
|
|
45
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.256",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.157",
|
|
47
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.19",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.129",
|
|
49
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.120"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
|
@@ -1,38 +1,14 @@
|
|
|
1
|
-
// Where the local platforms running on this machine say who they are
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// the tools exist for: a hand-started `pnpm run serve` on `local.db` beside the
|
|
8
|
-
// VS Code runner's child on `runner.db`, same app, same directory. Both then
|
|
9
|
-
// report success against different databases — a reset that empties an unserved
|
|
10
|
-
// store, and a seed that refuses because the served one is full.
|
|
11
|
-
//
|
|
12
|
-
// So a platform publishes itself instead: one JSON entry per run, next to the
|
|
13
|
-
// store it opened, removed on the way out. `LocalSeedTarget` turns that into the
|
|
14
|
-
// answer to "which platform do you mean".
|
|
15
|
-
//
|
|
16
|
-
// ── Why one file per port ──────────────────────────────────────────────────
|
|
17
|
-
//
|
|
18
|
-
// The pair that broke is two platforms of the SAME app in the SAME package
|
|
19
|
-
// directory, which is precisely what a single `platform.json` cannot represent.
|
|
20
|
-
// The domain port is what already distinguishes them (`REVENTLESS_DOMAIN_PORT`,
|
|
21
|
-
// added so the runner could start one child per app without colliding), so it
|
|
22
|
-
// names the file.
|
|
23
|
-
//
|
|
24
|
-
// ── Why the path is absolute ───────────────────────────────────────────────
|
|
25
|
-
//
|
|
26
|
-
// `./.reventless/local.db` names a different file in every app. Relative to the
|
|
27
|
-
// writer's cwd it is unambiguous and outside it means nothing, so the entry
|
|
28
|
-
// carries the path `BackendState` resolved, not the env string it came from.
|
|
1
|
+
// Where the local platforms running on this machine say who they are: one JSON
|
|
2
|
+
// entry per run under `<cwd>/.reventless/running/<domainPort>.json`, removed on
|
|
3
|
+
// exit. Keyed by domain port because the pair that broke is two platforms of the
|
|
4
|
+
// SAME app in one directory. Paths are absolute — `./.reventless/local.db` names
|
|
5
|
+
// a different file in every app. `LocalSeedTarget` turns this into the answer to
|
|
6
|
+
// "which platform do you mean".
|
|
29
7
|
|
|
30
8
|
@schema
|
|
31
9
|
type store = {
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// "memory": it has nothing on disk to open, which is the only distinction a
|
|
35
|
-
// tool acting from outside can act on.
|
|
10
|
+
// What a reader can DO with the store, not which module implements it —
|
|
11
|
+
// "sqlite" | "memory" | "postgres". A `:memory:` SQLite file reports "memory".
|
|
36
12
|
kind: string,
|
|
37
13
|
// Some only for a store this machine can open by name.
|
|
38
14
|
path: option<string>,
|
|
@@ -47,20 +23,21 @@ type entry = {
|
|
|
47
23
|
loginEndpoint: string,
|
|
48
24
|
store: store,
|
|
49
25
|
startedAt: string,
|
|
26
|
+
// Where the NDJSON event tap is served, when it is. Must stay optional: `list`
|
|
27
|
+
// DELETES an entry it cannot decode, so requiring it would silently hide every
|
|
28
|
+
// platform from an older build. Absent ⇒ stdout only.
|
|
29
|
+
tapPort?: int,
|
|
50
30
|
}
|
|
51
31
|
|
|
52
|
-
//
|
|
53
|
-
// `.reventless/` already lives. Threaded rather than read at each use so the
|
|
54
|
-
// tests can exercise a registry without moving the whole process.
|
|
32
|
+
// Threaded rather than read at each use, so tests need not move the process.
|
|
55
33
|
let runningDir = (~cwd=NodeProcess.cwd(), ()): string =>
|
|
56
34
|
NodePath.join([cwd, ".reventless", "running"])
|
|
57
35
|
|
|
58
36
|
let entryPath = (~port: int, ~cwd=NodeProcess.cwd()): string =>
|
|
59
37
|
NodePath.join([runningDir(~cwd, ()), `${port->Int.toString}.json`])
|
|
60
38
|
|
|
61
|
-
// The package
|
|
62
|
-
//
|
|
63
|
-
// are different apps and the port alone reads as an arbitrary number.
|
|
39
|
+
// The package name, for the prompt line — port alone reads as an arbitrary
|
|
40
|
+
// number when the platforms are different apps.
|
|
64
41
|
let appName = (~cwd=NodeProcess.cwd(), ()): string => {
|
|
65
42
|
let manifest = NodePath.join([cwd, "package.json"])
|
|
66
43
|
let fromManifest = if NodeFs.existsSync(manifest) {
|
|
@@ -85,11 +62,8 @@ let removeQuietly = (path: string): unit =>
|
|
|
85
62
|
| _ => ()
|
|
86
63
|
}
|
|
87
64
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Liveness by pid rather than by probing the endpoint keeps this synchronous,
|
|
92
|
-
and prunes a wedged platform that holds its port but would never answer. */
|
|
65
|
+
/** Signal `0` signals nothing — it runs the existence/permission checks and
|
|
66
|
+
throws when either fails. By pid, so a wedged platform is pruned too. */
|
|
93
67
|
let alive = (pid: int): bool =>
|
|
94
68
|
try {
|
|
95
69
|
NodeProcess.kill(pid, 0)
|
|
@@ -103,12 +77,8 @@ let readEntry = (path: string): option<entry> =>
|
|
|
103
77
|
| _ => None
|
|
104
78
|
}
|
|
105
79
|
|
|
106
|
-
/** Every platform
|
|
107
|
-
|
|
108
|
-
Removal on exit is best-effort — a `kill -9` leaves the entry behind — so
|
|
109
|
-
staleness is settled here, on read: an entry whose process is gone is deleted
|
|
110
|
-
rather than reported. A reader that trusted the file would offer a platform
|
|
111
|
-
nothing is serving, which is the failure this module exists to remove. */
|
|
80
|
+
/** Every platform running here, lowest port first. Removal on exit is
|
|
81
|
+
best-effort (`kill -9`), so a dead entry is deleted here, on read. */
|
|
112
82
|
let list = (~cwd=NodeProcess.cwd(), ()): array<entry> => {
|
|
113
83
|
let dir = runningDir(~cwd, ())
|
|
114
84
|
if !NodeFs.existsSync(dir) {
|
|
@@ -132,14 +102,14 @@ let list = (~cwd=NodeProcess.cwd(), ()): array<entry> => {
|
|
|
132
102
|
}
|
|
133
103
|
}
|
|
134
104
|
|
|
135
|
-
/** Writes one entry and
|
|
136
|
-
|
|
137
|
-
installing exit handlers in the test runner. */
|
|
105
|
+
/** Writes one entry and returns its path. Split from {!register} so a test can
|
|
106
|
+
populate a registry without installing exit handlers in the runner. */
|
|
138
107
|
let write = (
|
|
139
108
|
~port: int,
|
|
140
109
|
~endpoint: string,
|
|
141
110
|
~loginEndpoint: string,
|
|
142
111
|
~store: store,
|
|
112
|
+
~tapPort: option<int>=?,
|
|
143
113
|
~pid: int=NodeProcess.pid,
|
|
144
114
|
~cwd: string=NodeProcess.cwd(),
|
|
145
115
|
): string => {
|
|
@@ -152,6 +122,7 @@ let write = (
|
|
|
152
122
|
loginEndpoint,
|
|
153
123
|
store,
|
|
154
124
|
startedAt: Date.make()->Date.toISOString,
|
|
125
|
+
?tapPort,
|
|
155
126
|
}
|
|
156
127
|
try {
|
|
157
128
|
NodeFs.mkdirSync(runningDir(~cwd, ()), {recursive: true})
|
|
@@ -160,23 +131,43 @@ let write = (
|
|
|
160
131
|
entry->Reventless.Util_Sury.toJson(entrySchema)->JSON.stringify(~space=2),
|
|
161
132
|
)
|
|
162
133
|
} catch {
|
|
163
|
-
// A platform that cannot announce itself must still serve.
|
|
164
|
-
// to their endpoint default, which is what they did before this existed.
|
|
134
|
+
// A platform that cannot announce itself must still serve.
|
|
165
135
|
| _ => ()
|
|
166
136
|
}
|
|
167
137
|
path
|
|
168
138
|
}
|
|
169
139
|
|
|
170
|
-
/**
|
|
140
|
+
/** Adds the tap port to an entry already written.
|
|
141
|
+
|
|
142
|
+
Separate from {!write} because the socket binds asynchronously: the entry is
|
|
143
|
+
published as soon as the servers are up, and this fills in the port once
|
|
144
|
+
`listen` calls back. So the entry only ever names a port that is genuinely
|
|
145
|
+
listening. A no-op when there is no entry to update. */
|
|
146
|
+
let publishTapPort = (~port: int, ~tapPort: int, ~cwd=NodeProcess.cwd()): unit => {
|
|
147
|
+
let path = entryPath(~port, ~cwd)
|
|
148
|
+
switch readEntry(path) {
|
|
149
|
+
| None => ()
|
|
150
|
+
| Some(entry) =>
|
|
151
|
+
try NodeFs.writeFileSync(
|
|
152
|
+
path,
|
|
153
|
+
{...entry, tapPort}->Reventless.Util_Sury.toJson(entrySchema)->JSON.stringify(~space=2),
|
|
154
|
+
) catch {
|
|
155
|
+
| _ => ()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
171
159
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
160
|
+
/** Publishes this process, and unpublishes it on the way out. Both paths are
|
|
161
|
+
needed: `exit` is sync-only, and registering a signal handler suppresses
|
|
162
|
+
Node's default exit, so each signal must leave explicitly. */
|
|
163
|
+
let register = (
|
|
164
|
+
~port: int,
|
|
165
|
+
~endpoint: string,
|
|
166
|
+
~loginEndpoint: string,
|
|
167
|
+
~store: store,
|
|
168
|
+
~tapPort: option<int>=?,
|
|
169
|
+
): unit => {
|
|
170
|
+
let path = write(~port, ~endpoint, ~loginEndpoint, ~store, ~tapPort?)
|
|
180
171
|
NodeProcess.onExit(_ => removeQuietly(path))
|
|
181
172
|
NodeProcess.onSignal(#SIGINT, () => {
|
|
182
173
|
removeQuietly(path)
|
|
@@ -21,7 +21,8 @@ let entrySchema = Sury.$schema(s => ({
|
|
|
21
21
|
endpoint: s.m(Sury.string),
|
|
22
22
|
loginEndpoint: s.m(Sury.string),
|
|
23
23
|
store: s.m(storeSchema),
|
|
24
|
-
startedAt: s.m(Sury.string)
|
|
24
|
+
startedAt: s.m(Sury.string),
|
|
25
|
+
tapPort: s.m(Sury.$option(Sury.int))
|
|
25
26
|
}));
|
|
26
27
|
|
|
27
28
|
function runningDir(cwdOpt, param) {
|
|
@@ -108,7 +109,7 @@ function list(cwdOpt, param) {
|
|
|
108
109
|
}).toSorted((a, b) => Primitive_int.compare(a.port, b.port));
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function write(port, endpoint, loginEndpoint, store, pidOpt, cwdOpt) {
|
|
112
|
+
function write(port, endpoint, loginEndpoint, store, tapPort, pidOpt, cwdOpt) {
|
|
112
113
|
let pid = pidOpt !== undefined ? pidOpt : process.pid;
|
|
113
114
|
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
114
115
|
let path = entryPath(port, cwd);
|
|
@@ -121,7 +122,8 @@ function write(port, endpoint, loginEndpoint, store, pidOpt, cwdOpt) {
|
|
|
121
122
|
endpoint: endpoint,
|
|
122
123
|
loginEndpoint: loginEndpoint,
|
|
123
124
|
store: store,
|
|
124
|
-
startedAt: entry_startedAt
|
|
125
|
+
startedAt: entry_startedAt,
|
|
126
|
+
tapPort: tapPort
|
|
125
127
|
};
|
|
126
128
|
try {
|
|
127
129
|
Nodefs.mkdirSync(runningDir(cwd, undefined), {
|
|
@@ -134,8 +136,24 @@ function write(port, endpoint, loginEndpoint, store, pidOpt, cwdOpt) {
|
|
|
134
136
|
return path;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
function
|
|
138
|
-
let
|
|
139
|
+
function publishTapPort(port, tapPort, cwdOpt) {
|
|
140
|
+
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
141
|
+
let path = entryPath(port, cwd);
|
|
142
|
+
let entry = readEntry(path);
|
|
143
|
+
if (entry === undefined) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
let newrecord = {...entry};
|
|
148
|
+
Nodefs.writeFileSync(path, JSON.stringify(Util_Sury$Reventless.toJson((newrecord.tapPort = tapPort, newrecord), entrySchema), undefined, 2), "utf8");
|
|
149
|
+
return;
|
|
150
|
+
} catch (exn) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function register(port, endpoint, loginEndpoint, store, tapPort) {
|
|
156
|
+
let path = write(port, endpoint, loginEndpoint, store, tapPort, undefined, undefined);
|
|
139
157
|
process.on("exit", param => removeQuietly(path));
|
|
140
158
|
process.on("SIGINT", () => {
|
|
141
159
|
removeQuietly(path);
|
|
@@ -158,6 +176,7 @@ export {
|
|
|
158
176
|
readEntry,
|
|
159
177
|
list,
|
|
160
178
|
write,
|
|
179
|
+
publishTapPort,
|
|
161
180
|
register,
|
|
162
181
|
}
|
|
163
182
|
/* storeSchema Not a pure module */
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// What a starter asks the registry before it starts a local platform: whether a
|
|
2
|
+
// reset would wipe a store something is serving, and whether to start at all.
|
|
3
|
+
// Scoped to <cwd>/.reventless/running/, so another app's store file is not
|
|
4
|
+
// covered. See docs/plans/done/one-local-platform-one-store.md.
|
|
5
|
+
|
|
6
|
+
// Naming a port IS the statement that this platform is meant to coexist — the
|
|
7
|
+
// escape hatch the runner and the e2e suites already take.
|
|
8
|
+
let bypassEnv = "REVENTLESS_DOMAIN_PORT"
|
|
9
|
+
|
|
10
|
+
let envNamed = (key: string): bool =>
|
|
11
|
+
switch NodeProcess.env->Dict.get(key) {
|
|
12
|
+
| Some(v) if v->String.trim != "" => true
|
|
13
|
+
| _ => false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type decision =
|
|
17
|
+
| Start
|
|
18
|
+
| AlreadyRunning(array<LocalPlatformRegistry.entry>)
|
|
19
|
+
|
|
20
|
+
/** Whether this process should go on to build a platform. `~cwd` is threaded so a
|
|
21
|
+
test can exercise a registry without moving the whole process. */
|
|
22
|
+
let decide = (~cwd=NodeProcess.cwd(), ()): decision =>
|
|
23
|
+
if envNamed(bypassEnv) {
|
|
24
|
+
Start
|
|
25
|
+
} else {
|
|
26
|
+
switch LocalPlatformRegistry.list(~cwd, ()) {
|
|
27
|
+
| [] => Start
|
|
28
|
+
| entries => AlreadyRunning(entries)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Through LocalSeedTarget, so the starters and the seed tools describe the same
|
|
33
|
+
// platform the same way.
|
|
34
|
+
let entryLine = (e: LocalPlatformRegistry.entry): string =>
|
|
35
|
+
`→ already running at :${e.port->Int.toString} · ${e.store->LocalSeedTarget.storeLabel} (pid ${e.pid->Int.toString})`
|
|
36
|
+
|
|
37
|
+
/** What {!orAddressRunning} prints. Separate from the exit so it is testable. */
|
|
38
|
+
let report = (entries: array<LocalPlatformRegistry.entry>): array<string> =>
|
|
39
|
+
switch entries {
|
|
40
|
+
| [e] => [e->entryLine, ` nothing started — ${e.endpoint} is serving this directory.`]
|
|
41
|
+
| entries =>
|
|
42
|
+
Array.concat(
|
|
43
|
+
[`→ ${entries->Array.length->Int.toString} platforms are already running in this directory:`],
|
|
44
|
+
entries->Array.map(entryLine),
|
|
45
|
+
)->Array.concat([" nothing started — stop the ones you do not want, then start again."])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The live platform serving this store file, if one is. Resolved against this
|
|
49
|
+
process's cwd, because the registry's paths are absolute. */
|
|
50
|
+
let servedBy = (~path: string, ~cwd=NodeProcess.cwd(), ()): option<
|
|
51
|
+
LocalPlatformRegistry.entry,
|
|
52
|
+
> => {
|
|
53
|
+
let resolved = NodePath.resolve([path])
|
|
54
|
+
LocalPlatformRegistry.list(~cwd, ())->Array.find(e => e.store.path == Some(resolved))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let refusalMessage = (~path: string, e: LocalPlatformRegistry.entry): string =>
|
|
58
|
+
`refusing to reset ${path}: it is the store of ${e.app} running at :${e.port->Int.toString} (pid ${e.pid->Int.toString}). ` ++
|
|
59
|
+
`Stop that platform first, or start this one without ?reset.`
|
|
60
|
+
|
|
61
|
+
/** Throws rather than letting a reset unlink a store something is serving. Always
|
|
62
|
+
on: it must protect an app that never calls {!orAddressRunning}. */
|
|
63
|
+
let guardReset = (~path: string, ~cwd=NodeProcess.cwd(), ()): unit =>
|
|
64
|
+
switch servedBy(~path, ~cwd, ()) {
|
|
65
|
+
| Some(e) => JsError.throwWithMessage(refusalMessage(~path, e))
|
|
66
|
+
| None => ()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Start, or address the one already running. An app's `Main.res` calls this as
|
|
70
|
+
its first line, before `Platform.Make()`. */
|
|
71
|
+
let orAddressRunning = (~cwd=NodeProcess.cwd(), ()): unit => {
|
|
72
|
+
// The reset is checked first and throws: exit 0 is for a start that was merely
|
|
73
|
+
// redundant, never for a destructive one that did not happen.
|
|
74
|
+
switch Backend.fromEnv() {
|
|
75
|
+
| Backend.Sqlite({path, resetOnStart: true}) => guardReset(~path, ~cwd, ())
|
|
76
|
+
| Backend.Sqlite(_) | Backend.Memory | Backend.Postgres(_) => ()
|
|
77
|
+
}
|
|
78
|
+
switch decide(~cwd, ()) {
|
|
79
|
+
| Start => ()
|
|
80
|
+
| AlreadyRunning(entries) =>
|
|
81
|
+
report(entries)->Array.forEach(Console.log)
|
|
82
|
+
NodeProcess.exit(0)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodepath from "node:path";
|
|
4
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Backend$ReventlessLocal from "./adapter/Backend.res.mjs";
|
|
7
|
+
import * as LocalSeedTarget$ReventlessLocal from "./LocalSeedTarget.res.mjs";
|
|
8
|
+
import * as LocalPlatformRegistry$ReventlessLocal from "./LocalPlatformRegistry.res.mjs";
|
|
9
|
+
|
|
10
|
+
let bypassEnv = "REVENTLESS_DOMAIN_PORT";
|
|
11
|
+
|
|
12
|
+
function envNamed(key) {
|
|
13
|
+
let v = process.env[key];
|
|
14
|
+
if (v !== undefined) {
|
|
15
|
+
return v.trim() !== "";
|
|
16
|
+
} else {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function decide(cwdOpt, param) {
|
|
22
|
+
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
23
|
+
if (envNamed(bypassEnv)) {
|
|
24
|
+
return "Start";
|
|
25
|
+
}
|
|
26
|
+
let entries = LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined);
|
|
27
|
+
if (entries.length !== 0) {
|
|
28
|
+
return {
|
|
29
|
+
TAG: "AlreadyRunning",
|
|
30
|
+
_0: entries
|
|
31
|
+
};
|
|
32
|
+
} else {
|
|
33
|
+
return "Start";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function entryLine(e) {
|
|
38
|
+
return `→ already running at :` + e.port.toString() + ` · ` + LocalSeedTarget$ReventlessLocal.storeLabel(e.store) + ` (pid ` + e.pid.toString() + `)`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function report(entries) {
|
|
42
|
+
if (entries.length !== 1) {
|
|
43
|
+
return [`→ ` + entries.length.toString() + ` platforms are already running in this directory:`].concat(entries.map(entryLine)).concat([" nothing started — stop the ones you do not want, then start again."]);
|
|
44
|
+
}
|
|
45
|
+
let e = entries[0];
|
|
46
|
+
return [
|
|
47
|
+
entryLine(e),
|
|
48
|
+
` nothing started — ` + e.endpoint + ` is serving this directory.`
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function servedBy(path, cwdOpt, param) {
|
|
53
|
+
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
54
|
+
let resolved = Nodepath.resolve(path);
|
|
55
|
+
return LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).find(e => Primitive_object.equal(e.store.path, resolved));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function refusalMessage(path, e) {
|
|
59
|
+
return `refusing to reset ` + path + `: it is the store of ` + e.app + ` running at :` + e.port.toString() + ` (pid ` + e.pid.toString() + `). Stop that platform first, or start this one without ?reset.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function guardReset(path, cwdOpt, param) {
|
|
63
|
+
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
64
|
+
let e = servedBy(path, cwd, undefined);
|
|
65
|
+
if (e !== undefined) {
|
|
66
|
+
return Stdlib_JsError.throwWithMessage(refusalMessage(path, e));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function orAddressRunning(cwdOpt, param) {
|
|
71
|
+
let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
|
|
72
|
+
let match = Backend$ReventlessLocal.fromEnv();
|
|
73
|
+
if (typeof match === "object" && match.TAG === "Sqlite") {
|
|
74
|
+
if (match.resetOnStart) {
|
|
75
|
+
guardReset(match.path, cwd, undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let entries = decide(cwd, undefined);
|
|
79
|
+
if (typeof entries !== "object") {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
report(entries._0).forEach(prim => {
|
|
83
|
+
console.log(prim);
|
|
84
|
+
});
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
bypassEnv,
|
|
90
|
+
envNamed,
|
|
91
|
+
decide,
|
|
92
|
+
entryLine,
|
|
93
|
+
report,
|
|
94
|
+
servedBy,
|
|
95
|
+
refusalMessage,
|
|
96
|
+
guardReset,
|
|
97
|
+
orAddressRunning,
|
|
98
|
+
}
|
|
99
|
+
/* node:path Not a pure module */
|
package/src/Platform.res
CHANGED
|
@@ -17,6 +17,12 @@ let _ = ReventlessCore.EffectLogger.install
|
|
|
17
17
|
// LOG_LEVEL env var still overrides this (e.g. LOG_LEVEL=info or =silent).
|
|
18
18
|
ReventlessCore.EffectLogger.setDefaultMinLevel(ReventlessCore.Logger.Debug)
|
|
19
19
|
|
|
20
|
+
// Same reasoning, for the format: a person reads these logs, including when
|
|
21
|
+
// `dev:full`'s concurrently, `serve:watch`'s tsx or an IDE pipes them — which is
|
|
22
|
+
// where the TTY probe alone concludes "log collector" and emits JSON. An explicit
|
|
23
|
+
// REVENTLESS_LOG_FORMAT still wins.
|
|
24
|
+
Reventless.AnsiStyle.setDefaultFormat(#text)
|
|
25
|
+
|
|
20
26
|
let log = ReventlessCore.Logger.fromEnv()
|
|
21
27
|
|
|
22
28
|
// Module-level ref to hold the platform GraphQL server instance in split mode.
|
|
@@ -114,6 +120,10 @@ module MakeWithConfig = (
|
|
|
114
120
|
// full replay needs no checkpoint.
|
|
115
121
|
| Backend.Sqlite({path, resetOnStart}) =>
|
|
116
122
|
if resetOnStart {
|
|
123
|
+
// Before the unlink: the ports are bound at the very end of construction,
|
|
124
|
+
// so a second `serve:reset` used to wipe a served store and only then
|
|
125
|
+
// discover it could not listen.
|
|
126
|
+
LocalPlatformStart.guardReset(~path, ())
|
|
117
127
|
Backend.removeFileIfExists(path)
|
|
118
128
|
}
|
|
119
129
|
let db = SqliteDriver.openDb(~path)
|
|
@@ -1316,6 +1326,12 @@ module MakeWithConfig = (
|
|
|
1316
1326
|
~loginEndpoint=`http://localhost:${domainPort->Int.toString}/__inmemory/login`,
|
|
1317
1327
|
~store={kind, path},
|
|
1318
1328
|
)
|
|
1329
|
+
// After the register, and it fills the tap port in once the socket is bound:
|
|
1330
|
+
// the port is ephemeral by default, so it is not known until `listen` calls
|
|
1331
|
+
// back. The entry therefore never names a port that is not listening.
|
|
1332
|
+
LocalEventTap.start(~onBound=tapPort =>
|
|
1333
|
+
LocalPlatformRegistry.publishTapPort(~port=domainPort, ~tapPort)
|
|
1334
|
+
, ())
|
|
1319
1335
|
// Fire onPlatformDeployed after all servers are started so late-deployed
|
|
1320
1336
|
// plugins (e.g. PlatformInspector) have their handler refs populated.
|
|
1321
1337
|
ReventlessCore.Plugin_Helpers.firePlatformDeployedHook({
|
package/src/Platform.res.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect";
|
|
|
13
13
|
import * as Pulumi from "@pulumi/pulumi";
|
|
14
14
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
15
15
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
16
|
+
import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiStyle.res.mjs";
|
|
16
17
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
17
18
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
18
19
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
@@ -35,6 +36,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
|
|
|
35
36
|
import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
|
|
36
37
|
import * as Task_Builder$ReventlessLocal from "./components/Task_Builder.res.mjs";
|
|
37
38
|
import * as BakedManifest$ReventlessLocal from "./BakedManifest.res.mjs";
|
|
39
|
+
import * as LocalEventTap$ReventlessLocal from "./adapter/LocalEventTap.res.mjs";
|
|
38
40
|
import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
|
|
39
41
|
import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginBehavior.res.mjs";
|
|
40
42
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
@@ -59,6 +61,7 @@ import * as ProjectionPending$ReventlessLocal from "./adapter/ProjectionPending.
|
|
|
59
61
|
import * as ReadModel_Builder$ReventlessLocal from "./components/ReadModel_Builder.res.mjs";
|
|
60
62
|
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
|
|
61
63
|
import * as LocalEvents_Server$ReventlessLocal from "./adapter/Api/LocalEvents_Server.res.mjs";
|
|
64
|
+
import * as LocalPlatformStart$ReventlessLocal from "./LocalPlatformStart.res.mjs";
|
|
62
65
|
import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
|
|
63
66
|
import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
|
|
64
67
|
import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
|
|
@@ -102,6 +105,8 @@ import * as InboundTranslationResolvers_GraphQL$ReventlessLocal from "./adapter/
|
|
|
102
105
|
|
|
103
106
|
EffectLogger$ReventlessCore.setDefaultMinLevel("Debug");
|
|
104
107
|
|
|
108
|
+
AnsiStyle$Reventless.setDefaultFormat("text");
|
|
109
|
+
|
|
105
110
|
let log = Logger$ReventlessCore.fromEnv();
|
|
106
111
|
|
|
107
112
|
let platformGraphQLRef = {
|
|
@@ -141,6 +146,7 @@ function MakeWithConfig(Config) {
|
|
|
141
146
|
let resetOnStart = match.resetOnStart;
|
|
142
147
|
let path = match.path;
|
|
143
148
|
if (resetOnStart) {
|
|
149
|
+
LocalPlatformStart$ReventlessLocal.guardReset(path, undefined, undefined);
|
|
144
150
|
Backend$ReventlessLocal.removeFileIfExists(path);
|
|
145
151
|
}
|
|
146
152
|
let db = SqliteDriver$ReventlessLocal.openDb(path);
|
|
@@ -1117,7 +1123,8 @@ function MakeWithConfig(Config) {
|
|
|
1117
1123
|
LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
|
|
1118
1124
|
kind: match[0],
|
|
1119
1125
|
path: match[1]
|
|
1120
|
-
});
|
|
1126
|
+
}, undefined);
|
|
1127
|
+
LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
|
|
1121
1128
|
Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
|
|
1122
1129
|
name: "local",
|
|
1123
1130
|
environment: Pulumi.getStack(),
|
|
@@ -1889,6 +1896,7 @@ function Make($star) {
|
|
|
1889
1896
|
let resetOnStart = backend.resetOnStart;
|
|
1890
1897
|
let path = backend.path;
|
|
1891
1898
|
if (resetOnStart) {
|
|
1899
|
+
LocalPlatformStart$ReventlessLocal.guardReset(path, undefined, undefined);
|
|
1892
1900
|
Backend$ReventlessLocal.removeFileIfExists(path);
|
|
1893
1901
|
}
|
|
1894
1902
|
let db = SqliteDriver$ReventlessLocal.openDb(path);
|
|
@@ -2855,7 +2863,8 @@ function Make($star) {
|
|
|
2855
2863
|
LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
|
|
2856
2864
|
kind: match[0],
|
|
2857
2865
|
path: match[1]
|
|
2858
|
-
});
|
|
2866
|
+
}, undefined);
|
|
2867
|
+
LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
|
|
2859
2868
|
Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
|
|
2860
2869
|
name: "local",
|
|
2861
2870
|
environment: Pulumi.getStack(),
|