@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/service-recorder.ts
CHANGED
|
@@ -46,7 +46,7 @@ const record = (entry: Record<string, unknown>, then?: () => void): void => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const started = Date.now();
|
|
49
|
-
const child = spawn(command, args, { stdio: 'inherit', env });
|
|
49
|
+
const child = spawn(command, args, { windowsHide: true, stdio: 'inherit', env });
|
|
50
50
|
if (child.pid !== undefined) record({ event: 'world.service.started', pid: child.pid, parentPid: process.pid });
|
|
51
51
|
|
|
52
52
|
/** The log's last `bytes` (from a line start) as the rotation `<log>.1`: however far a runaway writer overshot the cap,
|
package/src/storage-capacity.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function storagePoolResolver(onFailure?: (failure: StorageProbeFailure) =
|
|
|
18
18
|
let pool: string | undefined;
|
|
19
19
|
if (process.platform === 'darwin') {
|
|
20
20
|
stage = 'volume lookup';
|
|
21
|
-
const df = execFileSync('/bin/df', ['-P', root], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
21
|
+
const df = execFileSync('/bin/df', ['-P', root], { windowsHide: true, encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
22
22
|
const disk = df.trim().split('\n').at(-1)?.trim().split(/\s+/)[0];
|
|
23
23
|
if (disk && /^\/dev\/disk\d+(?:s\d+)*$/.test(disk)) {
|
|
24
24
|
stage = 'volume metadata';
|
|
@@ -49,7 +49,7 @@ export function storagePoolResolver(onFailure?: (failure: StorageProbeFailure) =
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function diskInfo(disk: string): string {
|
|
52
|
-
return execFileSync('/usr/sbin/diskutil', ['info', '-plist', disk], {
|
|
52
|
+
return execFileSync('/usr/sbin/diskutil', ['info', '-plist', disk], { windowsHide: true,
|
|
53
53
|
encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'],
|
|
54
54
|
});
|
|
55
55
|
}
|
package/src/up-task.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type UpTaskReply = { instance: WorldInstance } | { error: string };
|
|
|
14
14
|
* is not the caller's to keep, so the boot completes and its outcome is written to the lifecycle log. */
|
|
15
15
|
export function superviseWorldUp(configId: string, options: UpWorldOptions): Promise<WorldInstance> {
|
|
16
16
|
return new Promise((done, reject) => {
|
|
17
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'up-task-worker')], {
|
|
17
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'up-task-worker')], { windowsHide: true,
|
|
18
18
|
cwd: process.cwd(), env: process.env, detached: process.platform !== 'win32',
|
|
19
19
|
stdio: ['ignore', 'inherit', 'inherit', 'ipc'], serialization: 'json',
|
|
20
20
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// WORLD ORIGINS for a local host (docs/contributing/architecture.md, "Viewing a World"): each World a
|
|
2
|
+
// browser steps into is reached at an origin of its own, `http://<world>--<org>.localhost:<port>`, so no
|
|
3
|
+
// World's page shares an origin with another World's session. Browsers and curl resolve `*.localhost`
|
|
4
|
+
// to loopback. Callers that present a token keep path addressing on the host's own origin.
|
|
5
|
+
import { worldOriginLabel } from './served-world.ts';
|
|
6
|
+
|
|
7
|
+
export const LOCAL_ORIGIN_BASE = 'localhost';
|
|
8
|
+
|
|
9
|
+
/** The origin a browser reaches `served` at on a local host listening on `port`, or null when its
|
|
10
|
+
* names make no label. */
|
|
11
|
+
export function localWorldOrigin(served: string, port: number, base = LOCAL_ORIGIN_BASE): string | null {
|
|
12
|
+
const label = worldOriginLabel(served);
|
|
13
|
+
return label ? `http://${label}.${base}:${port}` : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Which World a request's host names: undefined for a host that is not a World's origin (the host's
|
|
17
|
+
* own, path-addressed), null for a World-origin-shaped host naming no World served here. */
|
|
18
|
+
export function worldOfHost(hostname: string, served: Iterable<string>, base = LOCAL_ORIGIN_BASE): string | null | undefined {
|
|
19
|
+
const suffix = `.${base}`;
|
|
20
|
+
if (!hostname.endsWith(suffix)) return undefined;
|
|
21
|
+
const label = hostname.slice(0, -suffix.length);
|
|
22
|
+
for (const name of served) if (worldOriginLabel(name) === label) return name;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether a path under the console's base is a door of a World served here: a World whose org shares the console's
|
|
27
|
+
* name (`console/<world>`) has its doors at `/-/console/<world>/…`, and they are the World's, never the console's. */
|
|
28
|
+
export function worldDoorUnderConsole(path: string, served: (name: string) => boolean, consoleBase: string): boolean {
|
|
29
|
+
if (path !== consoleBase && !path.startsWith(`${consoleBase}/`)) return false;
|
|
30
|
+
const m = /^\/-\/([^/]+)\/([^/]+)/.exec(path);
|
|
31
|
+
return Boolean(m && served(`${m[1]}/${m[2]}`));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether a path on a World's origin belongs to that World (its wire, its doors, its mirrors) or to
|
|
35
|
+
* what every origin carries (`/-/ping`, the console): a World's origin serves no other World. */
|
|
36
|
+
export function pathOfWorld(path: string, served: string, consoleBase: string): boolean {
|
|
37
|
+
return path === '/-/ping' || path === consoleBase || path.startsWith(`${consoleBase}/`) || path.startsWith(`/${served}/`) || path.startsWith(`/-/${served}/`) || path === `/${served}` || path === `/-/${served}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// THE LOCAL VIEW: one World served for a person to step into — its doors, its mirrors, its branches
|
|
2
|
+
// ("as of" views) and the console, on one origin (docs/contributing/architecture.md, "Viewing a
|
|
3
|
+
// World"). `volter world view` is this; the runtime's `up` gains no step. A host that serves many
|
|
4
|
+
// Worlds (world-host) answers the same doors; this serves one, and the branches made of it.
|
|
5
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { loadWorldConfig } from './configs.ts';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { serveHttp, stateDirName } from '@volter/world-core';
|
|
9
|
+
import { LocalBranches } from './local-branches.ts';
|
|
10
|
+
import { mountWorld, type MountedWorld } from './served-world.ts';
|
|
11
|
+
import { localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from './world-origins.ts';
|
|
12
|
+
|
|
13
|
+
/** A console as a host mounts it (`@volter/world-console`): answer a request under `base`, or null. */
|
|
14
|
+
export type ViewConsole = { handle: (request: Request, base: string, opts?: { worldOrigin?: boolean }) => Promise<Response | null> };
|
|
15
|
+
export const VIEW_CONSOLE_BASE = '/-/console';
|
|
16
|
+
export type WorldView = { url: string; base: string; served: string; token: string; readToken: string; console: string | null; /** the World's own browser origin, when it has one */ origin: string | null; stop: () => Promise<void> };
|
|
17
|
+
|
|
18
|
+
/** The branch Worlds already under `dir` (`<dir>/<org>/<world>/.volter/world.json`). */
|
|
19
|
+
function branchRoots(dir: string): string[] {
|
|
20
|
+
if (!existsSync(dir)) return [];
|
|
21
|
+
const out: string[] = [];
|
|
22
|
+
for (const org of readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory())) {
|
|
23
|
+
for (const world of readdirSync(join(dir, org.name), { withFileTypes: true }).filter((e) => e.isDirectory())) {
|
|
24
|
+
const root = join(dir, org.name, world.name);
|
|
25
|
+
if (existsSync(join(root, stateDirName(), 'world.json'))) out.push(root);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function serveWorldView(name: string, opts: { root?: string; port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */ origins?: boolean } = {}): Promise<WorldView> {
|
|
32
|
+
const root = resolve(opts.root ?? process.cwd());
|
|
33
|
+
const host = opts.host ?? '127.0.0.1';
|
|
34
|
+
const worlds = new Map<string, MountedWorld>();
|
|
35
|
+
let loopback = ''; let port = 0;
|
|
36
|
+
const loopbackBind = host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
37
|
+
// each World's browser session on an origin of its own, where the view listens on loopback (*.localhost)
|
|
38
|
+
const origins = opts.origins ?? loopbackBind;
|
|
39
|
+
// bound to loopback with an origin per World, the view is for the person at this machine: its pages need no token
|
|
40
|
+
// (served-world.ts localSession). Without origins every World's pages share one, and none is handed a session.
|
|
41
|
+
const local = loopbackBind && origins;
|
|
42
|
+
// a non-loopback --host turns origins off by default: say so (the World and its branches then share one origin)
|
|
43
|
+
if (opts.origins === undefined && !origins) opts.announce?.(`origins off (bound to ${host}): the World and its branches share one origin, where a browser session is only path-scoped; bind loopback for an origin per World`);
|
|
44
|
+
const browserOrigin = origins ? (served: string) => (port ? localWorldOrigin(served, port) : null) : undefined;
|
|
45
|
+
const mountAt = (at: string): Promise<MountedWorld> => mountWorld(loadWorldConfig(join(at, stateDirName(), 'world.json'), at).config.id, { root: at, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}) });
|
|
46
|
+
const branches: LocalBranches = new LocalBranches({ dir: join(root, stateDirName(), 'branches'), origin: () => loopback, worlds, mount: mountAt, ...(opts.announce ? { announce: opts.announce } : {}) });
|
|
47
|
+
const world = await mountWorld(name, { root, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}) });
|
|
48
|
+
worlds.set(world.served, world);
|
|
49
|
+
const server = await serveHttp({
|
|
50
|
+
// a pushed blob (a release video) is one PUT: bodies up to 1 GiB, as `serve` takes them
|
|
51
|
+
hostname: host, port: opts.port ?? 0, maxRequestBodySize: 1024 * 1024 * 1024,
|
|
52
|
+
// this front serves a World and its branches; each World's own twins journal their requests
|
|
53
|
+
twinRequestJournal: false,
|
|
54
|
+
async fetch(request) {
|
|
55
|
+
const at = new URL(request.url); const path = at.pathname;
|
|
56
|
+
// a World's own origin serves that World (and the ping and the console) and no other
|
|
57
|
+
const own = origins ? worldOfHost(at.hostname, worlds.keys()) : undefined;
|
|
58
|
+
if (own === null) return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
|
|
59
|
+
// the bare address, typed into a browser, is the World's page: at its own origin when it has one
|
|
60
|
+
if (opts.console && path === '/' && request.method === 'GET' && (request.headers.get('accept') ?? '').includes('text/html')) {
|
|
61
|
+
const served = own ?? world.served; const page = `${VIEW_CONSOLE_BASE}/${served}`;
|
|
62
|
+
return Response.redirect(own ? `${at.origin}${page}` : `${browserOrigin?.(served) ?? at.origin}${page}`, 302);
|
|
63
|
+
}
|
|
64
|
+
if (own && !pathOfWorld(path, own, VIEW_CONSOLE_BASE)) return Response.json({ error: `this origin serves ${own} only` }, { status: 404 });
|
|
65
|
+
if (path === '/-/ping') return Response.json({ ok: true, worlds: own ? [own] : [...worlds.keys()], ...(opts.console ? { console: `${VIEW_CONSOLE_BASE}/` } : {}) });
|
|
66
|
+
if (opts.console && (path === VIEW_CONSOLE_BASE || path.startsWith(`${VIEW_CONSOLE_BASE}/`)) && !worldDoorUnderConsole(path, (n) => worlds.has(n), VIEW_CONSOLE_BASE)) {
|
|
67
|
+
// on a World's own origin the console keeps no token script can read (its session is the credential)
|
|
68
|
+
const answered = await opts.console.handle(request, VIEW_CONSOLE_BASE, own ? { worldOrigin: true } : undefined);
|
|
69
|
+
if (answered) return answered;
|
|
70
|
+
}
|
|
71
|
+
const m = /^\/(?:-\/)?([^/]+)\/([^/]+)/.exec(path);
|
|
72
|
+
const target = m ? worlds.get(`${m[1]}/${m[2]}`) : undefined;
|
|
73
|
+
if (!target) return Response.json({ error: `no world at ${path}: this view serves ${[...worlds.keys()].join(', ')}` }, { status: 404 });
|
|
74
|
+
return target.handle(request);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
port = server.port ?? 0;
|
|
78
|
+
const url = `http://${host}:${server.port}`;
|
|
79
|
+
loopback = `http://${host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host}:${server.port}`;
|
|
80
|
+
// a branch outlives the view that made it until its time runs out: stopping stops its compute only
|
|
81
|
+
const stop = async (): Promise<void> => {
|
|
82
|
+
branches.stop();
|
|
83
|
+
await server.stop(true);
|
|
84
|
+
for (const w of [...worlds.values()].reverse()) await w.stop();
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
await world.boot(url);
|
|
88
|
+
// branches an earlier view made: mounted again (the sweep then removes those whose time ran out)
|
|
89
|
+
for (const at of branchRoots(join(root, stateDirName(), 'branches'))) {
|
|
90
|
+
const b = await mountAt(at);
|
|
91
|
+
await b.boot(url);
|
|
92
|
+
worlds.set(b.served, b);
|
|
93
|
+
}
|
|
94
|
+
await branches.resume();
|
|
95
|
+
} catch (error) { await stop().catch(() => undefined); throw error; }
|
|
96
|
+
// the console opens on the World's own origin, where its session lives
|
|
97
|
+
const consoleUrl = opts.console ? `${browserOrigin?.(world.served) ?? url}${VIEW_CONSOLE_BASE}/${world.served}` : null;
|
|
98
|
+
opts.announce?.(`viewing ${world.served} ${url}/${world.served}${consoleUrl ? `\nconsole ${consoleUrl}` : ''}`);
|
|
99
|
+
return { url, base: `${url}/${world.served}`, served: world.served, get token() { return world.token; }, get readToken() { return world.readToken; }, console: consoleUrl, origin: browserOrigin?.(world.served) ?? null, stop };
|
|
100
|
+
}
|