@volter/world-runtime 2.0.0 → 2.0.2
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/dist/known-external-services.json +0 -8
- package/dist/src/app-url.js +1 -1
- package/dist/src/branch.js +79 -2
- package/dist/src/catalog.js +1 -1
- package/dist/src/cli.js +50 -22
- package/dist/src/console-apart.d.ts +1 -0
- package/dist/src/console-apart.js +7 -0
- package/dist/src/covers.js +4 -2
- package/dist/src/fixture-env.d.ts +3 -0
- package/dist/src/fixture-env.js +24 -0
- package/dist/src/host-worker.js +2 -9
- package/dist/src/host.js +2 -9
- package/dist/src/import-module.d.ts +1 -0
- package/dist/src/import-module.js +16 -0
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.js +4 -1
- package/dist/src/infra-cli.js +61 -12
- package/dist/src/init.d.ts +1 -1
- package/dist/src/init.js +34 -5
- package/dist/src/local-branches.d.ts +37 -0
- package/dist/src/local-branches.js +193 -0
- package/dist/src/pglite-backing.d.ts +8 -0
- package/dist/src/pglite-backing.js +121 -35
- package/dist/src/pglite-host.mjs +520 -14
- package/dist/src/prerequisites.js +1 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/redirect-proxy.d.ts +1 -1
- package/dist/src/redirect-proxy.js +4 -4
- package/dist/src/redis-backing.d.ts +8 -0
- package/dist/src/redis-backing.js +120 -0
- package/dist/src/root.d.ts +23 -0
- package/dist/src/root.js +22 -10
- package/dist/src/run-task.js +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +37 -20
- package/dist/src/schema.d.ts +5 -0
- package/dist/src/schema.js +10 -1
- package/dist/src/served-world.d.ts +196 -9
- package/dist/src/served-world.js +847 -103
- package/dist/src/service-recorder.js +1 -1
- package/dist/src/storage-capacity.js +2 -2
- package/dist/src/up-task.js +1 -1
- package/dist/src/world-origins.d.ts +13 -0
- package/dist/src/world-origins.js +37 -0
- package/dist/src/world-view.d.ts +25 -0
- package/dist/src/world-view.js +110 -0
- package/known-external-services.json +0 -8
- package/package.json +10 -4
- package/src/app-url.ts +1 -1
- package/src/branch.ts +66 -2
- package/src/catalog.ts +1 -1
- package/src/cli.ts +43 -21
- package/src/console-apart.ts +7 -1
- package/src/covers.ts +4 -2
- package/src/fixture-env.ts +25 -0
- package/src/host-worker.ts +2 -1
- package/src/host.ts +2 -1
- package/src/import-module.ts +9 -0
- package/src/index.ts +7 -2
- package/src/infra-cli.ts +56 -12
- package/src/init.ts +34 -5
- package/src/local-branches.ts +173 -0
- package/src/pglite-backing.ts +112 -36
- package/src/pglite-host.mjs +520 -14
- package/src/prerequisites.ts +1 -1
- package/src/process-groups.ts +1 -1
- package/src/redirect-proxy.ts +4 -4
- package/src/redis-backing.ts +107 -0
- package/src/root.ts +27 -2
- package/src/run-task.ts +1 -1
- package/src/runtime.ts +38 -20
- package/src/schema.ts +11 -1
- package/src/served-world.ts +762 -85
- package/src/service-recorder.ts +1 -1
- package/src/storage-capacity.ts +2 -2
- package/src/up-task.ts +1 -1
- package/src/world-origins.ts +38 -0
- package/src/world-view.ts +100 -0
package/src/pglite-backing.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
// The containerless backing for World-managed infrastructure
|
|
2
|
-
// (see pglite-host.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// The containerless backing for World-managed infrastructure, instead of docker
|
|
2
|
+
// compose: a POSTGRES service is a PGlite host (see pglite-host.mjs), a MONGODB
|
|
3
|
+
// service is the MongoDB twin (@volter/twin-mongodb: the MongoDB wire protocol over
|
|
4
|
+
// the World state kernel). Selected by `volter-world-infra` when no container
|
|
5
|
+
// runtime is available (or by explicit VOLTER_WORLD_INFRA_BACKING=pglite); the
|
|
6
|
+
// world config, definition file, injected env, and declared-service contract are
|
|
7
|
+
// byte-identical either way — backing is private, exactly as the boundary comment
|
|
8
|
+
// in infra-cli.ts promises. Each service's bytes live under VOLTER_WORLD_DATA, so
|
|
9
|
+
// they survive down/up as the compose volumes do.
|
|
8
10
|
//
|
|
9
|
-
// Capability is stated, not stretched: this backing serves POSTGRES
|
|
10
|
-
// only. A definition declaring mysql
|
|
11
|
-
// refused with the kinds named, never half-booted.
|
|
12
|
-
|
|
11
|
+
// Capability is stated, not stretched: this backing serves POSTGRES and MONGODB
|
|
12
|
+
// services only. A definition declaring mysql without a container runtime is
|
|
13
|
+
// refused with the kinds named, never half-booted. Redis never reaches it: every
|
|
14
|
+
// redis service is the redis twin's (redis-backing.ts).
|
|
15
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
13
16
|
import { connect } from 'node:net';
|
|
14
17
|
import { dirname, join } from 'node:path';
|
|
15
|
-
import { spawn } from 'node:child_process';
|
|
18
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { findInstalledPackage, packCli } from './catalog.ts';
|
|
16
21
|
|
|
17
22
|
export interface InfraService { kind: string; hostPort: number }
|
|
18
23
|
|
|
@@ -55,11 +60,50 @@ export function infraConnections(services: InfraService[], env: Record<string, s
|
|
|
55
60
|
return connections;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
// this module's directory under Node as under Bun: `volter-world-infra` is a node-shebang bin, where
|
|
64
|
+
// `import.meta.dir` (Bun's) is undefined
|
|
65
|
+
// lazy: a module that builds a URL from import.meta.url when it loads fails to load in workerd, which bundles this
|
|
66
|
+
// module into the hosted World (it never runs these services there)
|
|
67
|
+
const here = (): string => fileURLToPath(new URL('.', import.meta.url));
|
|
68
|
+
const hostScript = (): string => join(here(), 'pglite-host.mjs');
|
|
59
69
|
const READY_TIMEOUT_MS = 60_000;
|
|
70
|
+
const MONGODB_TWIN = '@volter/twin-mongodb';
|
|
71
|
+
|
|
72
|
+
/** The kinds this backing serves, and the name its files carry under the data dir. */
|
|
73
|
+
const CONTAINERLESS: Record<string, string> = { postgres: 'pglite-postgres', mongodb: 'twin-mongodb' };
|
|
60
74
|
|
|
61
75
|
function pidPath(dataDir: string, kind: string): string {
|
|
62
|
-
return join(dataDir, `pglite-${kind}.pid`);
|
|
76
|
+
return join(dataDir, `${CONTAINERLESS[kind] ?? `pglite-${kind}`}.pid`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The MongoDB twin's cli: the package installed above the World's config (an app repo), else the one
|
|
80
|
+
* this runtime was installed with (its dependency, or the checkout's workspace link). */
|
|
81
|
+
export function mongodbTwinCli(worldConfig?: string, froms: string[] = [...(worldConfig ? [dirname(worldConfig)] : []), here()]): string {
|
|
82
|
+
for (const from of froms) {
|
|
83
|
+
const dir = findInstalledPackage(from, MONGODB_TWIN);
|
|
84
|
+
const cli = dir === undefined ? undefined : packCli(dir);
|
|
85
|
+
if (cli !== undefined) return cli;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`the containerless mongodb service is served by ${MONGODB_TWIN} (an optional dependency of @volter/world-runtime), which is not installed above ${froms.join(' or ')} — install it, or run the World with a container runtime`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The runtime a twin's cli runs under. A published package's cli is JavaScript and runs under this
|
|
91
|
+
* process's own runtime; a checkout's is TypeScript whose kernel needs more than Node's type
|
|
92
|
+
* stripping, so it runs under Bun (this process's, or the one on PATH) — the repository's toolchain —
|
|
93
|
+
* and without Bun it is refused by name rather than started to fail. */
|
|
94
|
+
export function twinRunner(cli: string, bunOnPath: () => boolean = () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000, windowsHide: true }).status === 0, underBun = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'): string {
|
|
95
|
+
if (!cli.endsWith('.ts') || underBun) return process.execPath;
|
|
96
|
+
if (bunOnPath()) return 'bun';
|
|
97
|
+
throw new Error(`a checkout's MongoDB twin runs under bun, which is not on PATH (${cli} is TypeScript that Node cannot run)`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The argv that serves one service on its declared loopback port, with its bytes in `serviceData`. */
|
|
101
|
+
function hostArgv(service: InfraService, serviceData: string): string[] {
|
|
102
|
+
if (service.kind === 'mongodb') {
|
|
103
|
+
const cli = mongodbTwinCli(process.env.VOLTER_WORLD_CONFIG);
|
|
104
|
+
return [twinRunner(cli), cli, 'serve', '--port', String(service.hostPort), '--root', serviceData];
|
|
105
|
+
}
|
|
106
|
+
return ['node', hostScript(), '--port', String(service.hostPort), '--data', serviceData];
|
|
63
107
|
}
|
|
64
108
|
|
|
65
109
|
function alive(pid: number): boolean {
|
|
@@ -81,36 +125,68 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
81
125
|
|
|
82
126
|
/** The kinds this backing cannot serve, or [] when it can serve the world. */
|
|
83
127
|
export function unsupportedKinds(services: InfraService[]): string[] {
|
|
84
|
-
return [...new Set(services.filter((s) => s.kind
|
|
128
|
+
return [...new Set(services.filter((s) => CONTAINERLESS[s.kind] === undefined).map((s) => s.kind))];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The line each host prints once ITS OWN listener is bound: readiness is taken from the child, never
|
|
132
|
+
* from a connect to the port, which any other process holding the port would answer. */
|
|
133
|
+
function readyLine(kind: string, port: number): string {
|
|
134
|
+
return kind === 'mongodb' ? `listening on mongodb://127.0.0.1:${port}` : `serving postgres wire protocol on 127.0.0.1:${port}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function logSince(log: string, offset: number): string {
|
|
138
|
+
try { return readFileSync(log).subarray(offset).toString('utf8'); } catch { return ''; }
|
|
85
139
|
}
|
|
86
140
|
|
|
87
141
|
export async function pgliteUp(services: InfraService[], dataDir: string): Promise<void> {
|
|
142
|
+
// Every check before any start: a refused up starts nothing, so it never leaves the World half-booted.
|
|
143
|
+
const plan: Array<{ service: InfraService; name: string; serviceData: string; log: string; pidFile: string; argv: string[] }> = [];
|
|
88
144
|
for (const service of services) {
|
|
89
145
|
const pidFile = pidPath(dataDir, service.kind);
|
|
90
146
|
if (existsSync(pidFile) && alive(Number(readFileSync(pidFile, 'utf8').trim()))) continue;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const log = join(dataDir,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
const name = CONTAINERLESS[service.kind] ?? `pglite-${service.kind}`;
|
|
148
|
+
const serviceData = join(dataDir, `${name}-data`);
|
|
149
|
+
const log = join(dataDir, `${name}.log`);
|
|
150
|
+
if (await listening(service.hostPort)) {
|
|
151
|
+
throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
|
|
152
|
+
}
|
|
153
|
+
plan.push({ service, name, serviceData, log, pidFile, argv: hostArgv(service, serviceData) });
|
|
154
|
+
}
|
|
155
|
+
const started: Array<{ pid: number; pidFile: string }> = [];
|
|
156
|
+
try {
|
|
157
|
+
for (const { service, name, serviceData, log, pidFile, argv } of plan) {
|
|
158
|
+
mkdirSync(serviceData, { recursive: true });
|
|
159
|
+
mkdirSync(dirname(log), { recursive: true });
|
|
160
|
+
const offset = existsSync(log) ? statSync(log).size : 0;
|
|
161
|
+
const [command, ...args] = argv;
|
|
162
|
+
// a real fd, not a pipe: the host must outlive this process untethered
|
|
163
|
+
const logFd = openSync(log, 'a');
|
|
164
|
+
const child = spawn(command!, args, {
|
|
165
|
+
windowsHide: true,
|
|
166
|
+
detached: true,
|
|
167
|
+
stdio: ['ignore', logFd, logFd],
|
|
168
|
+
});
|
|
169
|
+
child.unref();
|
|
170
|
+
closeSync(logFd);
|
|
171
|
+
writeFileSync(pidFile, `${child.pid}\n`);
|
|
172
|
+
started.push({ pid: child.pid!, pidFile });
|
|
173
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
174
|
+
const line = readyLine(service.kind, service.hostPort);
|
|
175
|
+
// ready when OUR child says its listener is bound (a squatter that took the port after the check
|
|
176
|
+
// above makes the child fail its bind and exit, never print this) and the port answers
|
|
177
|
+
while (!(alive(child.pid!) && logSince(log, offset).includes(line) && await listening(service.hostPort))) {
|
|
178
|
+
if (!alive(child.pid!)) throw new Error(`${name} exited before serving — see ${log}`);
|
|
179
|
+
if (Date.now() > deadline) throw new Error(`${name} did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
|
|
180
|
+
await sleep(200);
|
|
111
181
|
}
|
|
112
|
-
await sleep(200);
|
|
113
182
|
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// tear down what this up started, so a failed up leaves nothing running
|
|
185
|
+
for (const { pid, pidFile } of started) {
|
|
186
|
+
if (alive(pid)) { try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ } }
|
|
187
|
+
rmSync(pidFile, { force: true });
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
114
190
|
}
|
|
115
191
|
}
|
|
116
192
|
|