@volter/world-runtime 2.0.15 → 2.0.17
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/src/infra-cli.js +7 -183
- package/dist/src/infra.d.ts +7 -0
- package/dist/src/infra.js +237 -0
- package/dist/src/pglite-backing.d.ts +10 -1
- package/dist/src/pglite-backing.js +10 -5
- package/dist/src/redis-backing.d.ts +2 -2
- package/dist/src/redis-backing.js +3 -1
- package/dist/src/runtime.js +51 -1
- package/dist/src/served-world.js +37 -12
- package/dist/src/world-view.js +2 -11
- package/package.json +4 -4
- package/src/infra-cli.ts +7 -174
- package/src/infra.ts +225 -0
- package/src/pglite-backing.ts +17 -5
- package/src/redis-backing.ts +4 -2
- package/src/runtime.ts +45 -1
- package/src/served-world.ts +26 -7
- package/src/world-view.ts +2 -5
package/src/runtime.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { createRequire } from 'node:module';
|
|
|
12
12
|
import { isIP } from 'node:net';
|
|
13
13
|
import { allocatePort } from './tcp-port.ts';
|
|
14
14
|
import { constants as osConstants, hostname as osHostname } from 'node:os';
|
|
15
|
-
import { appendFileSync, chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, existsSync } from 'node:fs';
|
|
15
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, existsSync } from 'node:fs';
|
|
16
16
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
17
17
|
import { packFacts } from './pack-facts.ts';
|
|
18
18
|
import { describeServiceEnd, readServiceEnd, serviceOutput } from './service-exit.ts';
|
|
@@ -832,6 +832,7 @@ async function runExternalUp(
|
|
|
832
832
|
logPath: string,
|
|
833
833
|
abort?: AbortSignal,
|
|
834
834
|
): Promise<string> {
|
|
835
|
+
if (ownInfrastructureUp(command)) return runOwnInfrastructureUp(command, serviceId, env, logPath, abort);
|
|
835
836
|
const [bin, ...args] = ownCommand(command);
|
|
836
837
|
if (!bin) throw new Error(`External service "${serviceId}": up command is empty`);
|
|
837
838
|
if (!commandExists(bin)) {
|
|
@@ -867,6 +868,41 @@ async function runExternalUp(
|
|
|
867
868
|
return chunks.join('');
|
|
868
869
|
}
|
|
869
870
|
|
|
871
|
+
/** Whether an external `up` is this runtime's own managed-infrastructure tool (`volter-world-infra up`, which `init`
|
|
872
|
+
* names), with nothing on PATH answering the name: the phase then runs in this process (infra.ts) instead of in a
|
|
873
|
+
* child. The services it starts (the PGlite host, the redis and MongoDB twins) are then children of the process that
|
|
874
|
+
* owns the World's lifetime, as every service recorder is, not of a tool that exits once they serve. Where a child
|
|
875
|
+
* does not outlive its parent (a browser tab's process layer, measured 2026-09-28: a detached, unref'd listener
|
|
876
|
+
* ended with the process that spawned it), the PGlite host ended the moment `volter-world-infra up` returned and
|
|
877
|
+
* the readiness probe timed out at "0/1 declared services are ready". status and down still run as the tool.
|
|
878
|
+
* The phase gets what the tool's process got: the infrastructure service's environment (`env`, which the hosts it
|
|
879
|
+
* starts are spawned with, never this process's own) and the boot's abort; its commands (docker info, compose)
|
|
880
|
+
* run without blocking this process's event loop. */
|
|
881
|
+
function ownInfrastructureUp(command: string[]): boolean {
|
|
882
|
+
return command[0] === 'volter-world-infra' && command[1] === 'up' && ownCommand(command)[0] === process.execPath;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function runOwnInfrastructureUp(command: string[], serviceId: string, env: NodeJS.ProcessEnv, logPath: string, abort?: AbortSignal): Promise<string> {
|
|
886
|
+
writeFileSync(logPath, `$ ${command.join(' ')}\n`, { mode: 0o600 });
|
|
887
|
+
chmodSync(logPath, 0o600);
|
|
888
|
+
const out = openPrivateLog(logPath, 'a');
|
|
889
|
+
const chunks: string[] = [];
|
|
890
|
+
try {
|
|
891
|
+
abort?.throwIfAborted();
|
|
892
|
+
const { runInfrastructure } = await import('./infra.ts');
|
|
893
|
+
const write = (text: string) => { chunks.push(text); writeSync(out, text); };
|
|
894
|
+
const code = await runInfrastructure(command[1]!, env, { out: write, err: write }, abort);
|
|
895
|
+
abort?.throwIfAborted();
|
|
896
|
+
if (code !== 0) {
|
|
897
|
+
const detail = chunks.join('').trim().split('\n').slice(-40).join('\n');
|
|
898
|
+
throw new Error(`External service "${serviceId}": up command exited ${code}\n${detail}`);
|
|
899
|
+
}
|
|
900
|
+
} finally {
|
|
901
|
+
closeSync(out);
|
|
902
|
+
}
|
|
903
|
+
return chunks.join('');
|
|
904
|
+
}
|
|
905
|
+
|
|
870
906
|
/** Resolve `${url}`/`${host}`/`${port}` in a readiness probe's httpUrl against an owned loopback
|
|
871
907
|
* service's allocated port (process/twin readiness — the port is auto-assigned, so it can't be a
|
|
872
908
|
* literal in the config). Other probe kinds (command/stdoutMatch) are returned unchanged. */
|
|
@@ -2207,7 +2243,15 @@ export function ownCommand(command: string[]): string[] {
|
|
|
2207
2243
|
return [process.execPath, siblingScript(import.meta.url, stem), ...args];
|
|
2208
2244
|
}
|
|
2209
2245
|
|
|
2246
|
+
/** A command answers when it names an executable: a path is checked as a file (what `which` does with a path),
|
|
2247
|
+
* never by spawning `which`. `ownCommand` hands this the running executable's own path (process.execPath), and a
|
|
2248
|
+
* process layer without synchronous spawn (a browser tab's, where spawnSync is ENOSYS) answered "not found on PATH"
|
|
2249
|
+
* for the very node running the World: the managed infrastructure never started there. A bare name is still looked
|
|
2250
|
+
* up on PATH. */
|
|
2210
2251
|
function onPath(command: string): boolean {
|
|
2252
|
+
if (command.includes(sep) || command.includes('/')) {
|
|
2253
|
+
try { accessSync(command, fsConstants.X_OK); return statSync(command).isFile(); } catch { return false; }
|
|
2254
|
+
}
|
|
2211
2255
|
return spawnSync('which', [command], { windowsHide: true, stdio: 'ignore', env: process.env }).status === 0;
|
|
2212
2256
|
}
|
|
2213
2257
|
|
package/src/served-world.ts
CHANGED
|
@@ -161,7 +161,8 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
|
|
|
161
161
|
const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
|
|
162
162
|
const keep = (): void => { writeSecret(join(worldRoot, stateDirName(), 'token'), world.token); writeSecret(join(worldRoot, stateDirName(), 'token.read'), world.readToken); };
|
|
163
163
|
let timers: ReturnType<typeof setInterval>[] = [];
|
|
164
|
-
|
|
164
|
+
let stopping: Promise<void> | null = null;
|
|
165
|
+
const mounted: MountedWorld = {
|
|
165
166
|
name, served, root: worldRoot,
|
|
166
167
|
get token() { return world.token; }, get readToken() { return world.readToken; },
|
|
167
168
|
twins: () => world.twins(),
|
|
@@ -188,13 +189,29 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
|
|
|
188
189
|
throw error;
|
|
189
190
|
}
|
|
190
191
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (result.externalErrors.length) throw new Error(`World "${name}" shutdown incomplete; instance and reservation retained: ${result.externalErrors.join('; ')}`);
|
|
192
|
+
stop() {
|
|
193
|
+
// one stop at a time: a second caller (the view shutting down while a switch stops this World) shares the first's
|
|
194
|
+
stopping ??= stopOnce().finally(() => { stopping = null; });
|
|
195
|
+
return stopping;
|
|
196
196
|
},
|
|
197
197
|
};
|
|
198
|
+
async function stopOnce(): Promise<void> {
|
|
199
|
+
// refreshes stop first, so none writes into the World while its twins stop; the serve record goes only once they are
|
|
200
|
+
// down. A stop that fails keeps the record (a checkout under the serve still refuses) and re-arms the refreshes:
|
|
201
|
+
// refused before anything stopped, the World is served as it was; failed part way, its record names what remains
|
|
202
|
+
for (const t of timers) clearInterval(t);
|
|
203
|
+
timers = [];
|
|
204
|
+
let result: Awaited<ReturnType<typeof downWorld>>;
|
|
205
|
+
try {
|
|
206
|
+
result = await downWorld(name, worldRoot);
|
|
207
|
+
if (result.externalErrors.length) throw new Error(`World "${name}" shutdown incomplete; instance and reservation retained: ${result.externalErrors.join('; ')}`);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (instance) { try { timers = await scheduleRefreshes(name, worldRoot, instance); } catch { /* the stop's error is the one reported */ } }
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
try { rmSync(record, { force: true }); } catch { /* already gone */ }
|
|
213
|
+
}
|
|
214
|
+
return mounted;
|
|
198
215
|
}
|
|
199
216
|
|
|
200
217
|
/** A local World's mirrors: each pack's builders, found once per process, and each client built once. */
|
|
@@ -1328,7 +1345,9 @@ export class WorldDoors {
|
|
|
1328
1345
|
for (const vendor of Object.keys(this.up().services)) {
|
|
1329
1346
|
const state = this.stateOf(vendor); const controlRoot = this.controlRoot(vendor);
|
|
1330
1347
|
const counts = new Map<string, number>();
|
|
1331
|
-
|
|
1348
|
+
// what the World holds as the vendor's own resources: the twin's bookkeeping (a `_` type, as a screens sign-in's token)
|
|
1349
|
+
// is never a resource a person reads, as it is in no log or diff
|
|
1350
|
+
for (const r of readTree(state, controlRoot)) if (r.deleted !== true && !r.type.startsWith('_')) counts.set(r.type, (counts.get(r.type) ?? 0) + 1);
|
|
1332
1351
|
twins.push({
|
|
1333
1352
|
twin: vendor, position: wholeLog(state, controlRoot).length,
|
|
1334
1353
|
mirror: Boolean(await this.host.hasMirror?.(vendor)),
|
package/src/world-view.ts
CHANGED
|
@@ -55,11 +55,8 @@ export async function serveWorldView(name: string, opts: { root?: string; port?:
|
|
|
55
55
|
switching = true;
|
|
56
56
|
const from = world.name;
|
|
57
57
|
try {
|
|
58
|
-
// a stop that
|
|
59
|
-
|
|
60
|
-
try { await world.boot(url); } catch { /* the stop's own error is the one reported */ }
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
58
|
+
// a stop that is refused or fails leaves this branch served as it was (served-world.ts stop): the switch just fails
|
|
59
|
+
await world.stop();
|
|
63
60
|
let next: MountedWorld;
|
|
64
61
|
try {
|
|
65
62
|
next = await mountWorld(branch, mountOpts);
|