@volter/world-runtime 3.0.38 → 3.0.39
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 +3 -10
- package/dist/src/actions-runner-cli.js +3 -3
- package/dist/src/actions-runner.js +2 -1
- package/dist/src/app-url.js +2 -1
- package/dist/src/backings.d.ts +10 -1
- package/dist/src/backings.js +66 -7
- package/dist/src/branch.js +85 -52
- package/dist/src/browser-routes.cjs +59 -0
- package/dist/src/ca-trust.js +4 -3
- package/dist/src/changeset.js +4 -3
- package/dist/src/cli-commands.js +25 -9
- package/dist/src/cli.js +2 -1
- package/dist/src/command-lifetime.d.mts +12 -0
- package/dist/src/command-lifetime.d.ts +1 -15
- package/dist/src/command-lifetime.js +1 -98
- package/dist/src/command-lifetime.mjs +90 -0
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.js +3 -2
- package/dist/src/covers.d.ts +3 -2
- package/dist/src/covers.js +95 -18
- package/dist/src/error-message.d.mts +1 -0
- package/dist/src/error-message.d.ts +1 -0
- package/dist/src/error-message.js +2 -0
- package/dist/src/error-message.mjs +10 -0
- package/dist/src/guest-command.d.mts +4 -0
- package/dist/src/guest-command.mjs +26 -0
- package/dist/src/guest-control.d.mts +1 -0
- package/dist/src/guest-control.mjs +17 -0
- package/dist/src/guest-entry.mjs +146 -0
- package/dist/src/guest-processes.d.mts +9 -0
- package/dist/src/guest-processes.mjs +47 -0
- package/dist/src/guest-runner.d.ts +6 -0
- package/dist/src/guest-runner.js +567 -0
- package/dist/src/host.js +5 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/infra-cli.js +4 -0
- package/dist/src/infra.js +133 -23
- package/dist/src/init.js +60 -23
- package/dist/src/lifecycle-record.d.ts +3 -1
- package/dist/src/lifecycle-record.js +29 -6
- package/dist/src/livekit-backing.mjs +37 -0
- package/dist/src/local-branches.js +11 -3
- package/dist/src/mongodb-backing.d.ts +7 -0
- package/dist/src/mongodb-backing.js +58 -0
- package/dist/src/native-postgres-backing.d.ts +4 -0
- package/dist/src/native-postgres-backing.js +404 -0
- package/dist/src/origin.js +2 -1
- package/dist/src/owned-path.d.ts +4 -0
- package/dist/src/owned-path.js +37 -0
- package/dist/src/pack-facts.d.ts +1 -0
- package/dist/src/pglite-backing.d.ts +4 -13
- package/dist/src/pglite-backing.js +198 -106
- package/dist/src/pglite-host.mjs +47 -17
- package/dist/src/placeholder.d.ts +3 -2
- package/dist/src/placeholder.js +9 -4
- package/dist/src/process-groups.d.mts +2 -0
- package/dist/src/process-groups.d.ts +1 -4
- package/dist/src/process-groups.js +1 -49
- package/dist/src/process-groups.mjs +59 -0
- package/dist/src/process-receipt.d.mts +6 -0
- package/dist/src/process-receipt.d.ts +11 -0
- package/dist/src/process-receipt.js +3 -0
- package/dist/src/process-receipt.mjs +45 -0
- package/dist/src/process-signal.d.mts +9 -0
- package/dist/src/process-signal.mjs +92 -0
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +4 -2
- package/dist/src/proxy-daemon.js +2 -1
- package/dist/src/redirect-proxy.js +186 -19
- package/dist/src/redis-backing.js +99 -35
- package/dist/src/reflect.js +2 -1
- package/dist/src/root.js +7 -6
- package/dist/src/run-task-worker.js +2 -1
- package/dist/src/runtime.d.ts +24 -7
- package/dist/src/runtime.js +703 -286
- package/dist/src/schema.d.ts +12 -1
- package/dist/src/schema.js +20 -5
- package/dist/src/served-world.d.ts +1 -0
- package/dist/src/served-world.js +26 -14
- package/dist/src/service-recorder.js +8 -3
- package/dist/src/signals.d.mts +1 -0
- package/dist/src/signals.d.ts +1 -1
- package/dist/src/signals.js +1 -11
- package/dist/src/signals.mjs +10 -0
- package/dist/src/tail.d.ts +2 -0
- package/dist/src/tail.js +9 -2
- package/dist/src/tcp-port.js +27 -13
- package/dist/src/temporal-backing.mjs +28 -0
- package/dist/src/up-cli.js +8 -3
- package/dist/src/up-task-worker.js +2 -1
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/websocket-relay.js +23 -10
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +3 -10
- package/package.json +12 -7
- package/src/actions-runner-cli.ts +3 -3
- package/src/actions-runner.ts +2 -1
- package/src/app-url.ts +2 -1
- package/src/backings.ts +65 -7
- package/src/branch.ts +73 -34
- package/src/browser-routes.cjs +59 -0
- package/src/ca-trust.ts +4 -3
- package/src/changeset.ts +4 -3
- package/src/cli-commands.ts +21 -9
- package/src/cli.ts +2 -1
- package/src/command-lifetime.d.mts +12 -0
- package/src/command-lifetime.mjs +90 -0
- package/src/command-lifetime.ts +1 -79
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +2 -1
- package/src/covers.ts +97 -19
- package/src/error-message.d.mts +1 -0
- package/src/error-message.mjs +10 -0
- package/src/error-message.ts +2 -0
- package/src/guest-command.d.mts +4 -0
- package/src/guest-command.mjs +26 -0
- package/src/guest-control.d.mts +1 -0
- package/src/guest-control.mjs +17 -0
- package/src/guest-entry.mjs +146 -0
- package/src/guest-processes.d.mts +9 -0
- package/src/guest-processes.mjs +47 -0
- package/src/guest-runner.ts +414 -0
- package/src/host.ts +5 -4
- package/src/index.ts +1 -0
- package/src/infra-cli.ts +4 -0
- package/src/infra.ts +103 -22
- package/src/init.ts +55 -22
- package/src/lifecycle-record.ts +21 -6
- package/src/livekit-backing.mjs +37 -0
- package/src/local-branches.ts +6 -3
- package/src/mongodb-backing.ts +55 -0
- package/src/native-postgres-backing.ts +329 -0
- package/src/origin.ts +2 -1
- package/src/owned-path.ts +32 -0
- package/src/pack-facts.ts +1 -0
- package/src/pglite-backing.ts +155 -79
- package/src/pglite-host.mjs +47 -17
- package/src/placeholder.ts +10 -5
- package/src/process-groups.d.mts +2 -0
- package/src/process-groups.mjs +59 -0
- package/src/process-groups.ts +1 -33
- package/src/process-receipt.d.mts +6 -0
- package/src/process-receipt.mjs +45 -0
- package/src/process-receipt.ts +5 -0
- package/src/process-signal.d.mts +9 -0
- package/src/process-signal.mjs +92 -0
- package/src/project-inspect.ts +4 -2
- package/src/proxy-daemon.ts +2 -1
- package/src/redirect-proxy.ts +153 -18
- package/src/redis-backing.ts +74 -25
- package/src/reflect.ts +2 -1
- package/src/root.ts +7 -6
- package/src/run-task-worker.ts +2 -1
- package/src/runtime.ts +596 -265
- package/src/schema.ts +30 -6
- package/src/served-world.ts +21 -15
- package/src/service-recorder.ts +4 -2
- package/src/signals.d.mts +1 -0
- package/src/signals.mjs +10 -0
- package/src/signals.ts +1 -10
- package/src/tail.ts +10 -3
- package/src/tcp-port.ts +24 -13
- package/src/temporal-backing.mjs +28 -0
- package/src/up-cli.ts +6 -3
- package/src/up-task-worker.ts +2 -1
- package/src/websocket-relay.ts +18 -11
- package/src/world-view.ts +2 -2
package/dist/src/infra.js
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
// the moment `volter-world-infra up` returned, and the World's readiness probe never saw it.
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
|
+
import { signalChild } from './process-signal.mjs';
|
|
12
13
|
import { spawn } from 'node:child_process';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
|
|
16
|
+
import { errorMessage } from "./error-message.js";
|
|
15
17
|
/** A command run to its end without blocking this process: in the World's own `up` the phase shares the event loop
|
|
16
18
|
* with the boot's signal handling, where the tool's spawnSync blocked only the tool. Ended by `timeout` (SIGTERM,
|
|
17
19
|
* as spawnSync's timeout) or by the boot's abort. */
|
|
@@ -21,11 +23,21 @@ function runCommand(command, args, options) {
|
|
|
21
23
|
let stderr = '';
|
|
22
24
|
let settled = false;
|
|
23
25
|
let grace;
|
|
24
|
-
const child = spawn(command, args, { windowsHide: true, env: options.env, stdio: ['ignore', 'pipe', 'pipe']
|
|
26
|
+
const child = spawn(command, args, { windowsHide: true, env: options.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
27
|
+
const abort = () => {
|
|
28
|
+
try {
|
|
29
|
+
if (child.pid)
|
|
30
|
+
signalChild(child, 'SIGTERM');
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
settle({ status: null, stdout, stderr, error: error });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
25
36
|
const settle = (ran) => {
|
|
26
37
|
if (settled)
|
|
27
38
|
return;
|
|
28
39
|
settled = true;
|
|
40
|
+
options.signal?.removeEventListener('abort', abort);
|
|
29
41
|
clearTimeout(timer);
|
|
30
42
|
if (grace)
|
|
31
43
|
clearTimeout(grace);
|
|
@@ -37,7 +49,14 @@ function runCommand(command, args, options) {
|
|
|
37
49
|
// the timeout is a hard limit and answers as spawnSync's did: the command is killed and the result carries an
|
|
38
50
|
// ETIMEDOUT error (a hung `docker info`, a daemon that never answers, is then no container runtime, as before)
|
|
39
51
|
const timer = setTimeout(() => {
|
|
40
|
-
|
|
52
|
+
try {
|
|
53
|
+
if (child.pid)
|
|
54
|
+
signalChild(child, 'SIGTERM');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
settle({ status: null, stdout, stderr, error: error });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
41
60
|
settle({ status: null, stdout, stderr, error: Object.assign(new Error(`spawn ${command} ETIMEDOUT`), { code: 'ETIMEDOUT' }) });
|
|
42
61
|
}, options.timeout);
|
|
43
62
|
child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
@@ -48,6 +67,9 @@ function runCommand(command, args, options) {
|
|
|
48
67
|
// answer by at most a moment, never to the timeout
|
|
49
68
|
child.on('exit', (code) => { grace = setTimeout(() => settle({ status: code, stdout, stderr }), 500); });
|
|
50
69
|
child.on('close', (code) => settle({ status: code, stdout, stderr }));
|
|
70
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
71
|
+
if (options.signal?.aborted)
|
|
72
|
+
abort();
|
|
51
73
|
});
|
|
52
74
|
}
|
|
53
75
|
/** One phase of the World-managed infrastructure. `env` is the infrastructure service's environment, which the
|
|
@@ -82,32 +104,52 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
82
104
|
: /cannot connect|not running|daemon/iu.test(raw)
|
|
83
105
|
? 'local execution capacity is unavailable'
|
|
84
106
|
: 'managed infrastructure operation failed';
|
|
107
|
+
// The existing backend redaction precedes truncation so a cut cannot expose part of a secret.
|
|
108
|
+
const diagnostic = (raw) => {
|
|
109
|
+
let detail = raw;
|
|
110
|
+
const secrets = Object.entries(env).filter(([name, value]) => value && value.length >= 4 && /secret|token|password|credential|api_key|database_url/iu.test(name))
|
|
111
|
+
.map(([, value]) => value).sort((a, b) => b.length - a.length);
|
|
112
|
+
for (const secret of secrets)
|
|
113
|
+
detail = detail.split(secret).join('[redacted]');
|
|
114
|
+
detail = detail.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+(?::[^\s/@]*)?@/giu, '$1[redacted]@');
|
|
115
|
+
return detail.split('\n').slice(-40).join('\n').slice(-8192);
|
|
116
|
+
};
|
|
85
117
|
const fail = (result) => {
|
|
86
|
-
const raw =
|
|
118
|
+
const raw = [result.stdout, result.stderr, result.error ? errorMessage(result.error) : undefined].filter(Boolean).join('\n').trim();
|
|
119
|
+
const detail = diagnostic(raw);
|
|
87
120
|
io.err(`managed infrastructure ${phase} failed: ${classify(raw)}\n`);
|
|
121
|
+
if (detail)
|
|
122
|
+
io.err(`backend diagnostic:\n${detail}\n`);
|
|
88
123
|
return result.status && result.status > 0 ? result.status : 1;
|
|
89
124
|
};
|
|
90
125
|
// ---- backing selection ------------------------------------------------------
|
|
91
126
|
// The declared service contract (up/status/down) is the boundary; WHICH
|
|
92
127
|
// runtime answers it is private and chosen here, per machine, at each phase:
|
|
93
|
-
// 1. VOLTER_WORLD_INFRA_BACKING=docker|pglite — explicit, for tests/operators;
|
|
128
|
+
// 1. VOLTER_WORLD_INFRA_BACKING=docker|pglite|native — explicit, for tests/operators;
|
|
94
129
|
// 2. a working container runtime — the compose path, byte-identical to before;
|
|
95
130
|
// 3. no container runtime + a definition of postgres and/or mongodb services —
|
|
96
131
|
// the containerless backing (pglite-backing.ts: PGlite for postgres, the
|
|
97
|
-
// MongoDB
|
|
132
|
+
// MongoDB's own server for mongodb), announced loudly;
|
|
98
133
|
// 4. otherwise the honest refusal naming what this machine cannot serve.
|
|
99
134
|
async function selectBacking() {
|
|
100
135
|
const forced = env.VOLTER_WORLD_INFRA_BACKING;
|
|
101
|
-
if (forced === 'docker' || forced === 'pglite')
|
|
136
|
+
if (forced === 'docker' || forced === 'pglite' || forced === 'native')
|
|
102
137
|
return forced;
|
|
103
138
|
if (forced !== undefined) {
|
|
104
|
-
io.err(`managed infrastructure: VOLTER_WORLD_INFRA_BACKING must be docker or
|
|
139
|
+
io.err(`managed infrastructure: VOLTER_WORLD_INFRA_BACKING must be docker, pglite or native (got ${JSON.stringify(forced)})\n`);
|
|
105
140
|
return 2;
|
|
106
141
|
}
|
|
107
142
|
const probe = await runCommand('docker', ['info', '--format', '{{.ServerVersion}}'], { env, timeout: 10_000, ...(signal ? { signal } : {}) });
|
|
108
143
|
signal?.throwIfAborted();
|
|
109
|
-
if (probe.status === 0)
|
|
144
|
+
if (probe.status === 0 && probe.stdout.trim())
|
|
110
145
|
return 'docker';
|
|
146
|
+
if (probe.status === 0) {
|
|
147
|
+
io.err('managed infrastructure: container runtime probe returned no server version; using containerless backing\n');
|
|
148
|
+
return 'pglite';
|
|
149
|
+
}
|
|
150
|
+
const probeOutput = [probe.stdout, probe.stderr, probe.error ? errorMessage(probe.error) : undefined].filter(Boolean).join('\n').trim();
|
|
151
|
+
if (probeOutput)
|
|
152
|
+
io.err(`managed infrastructure: container runtime probe failed\n${probeOutput}\n`);
|
|
111
153
|
// Absence of a container runtime (no binary, no daemon) is a capability
|
|
112
154
|
// difference: swap backings. A PRESENT runtime failing on resources stays
|
|
113
155
|
// on the docker path so the real capacity problem is classified and
|
|
@@ -127,7 +169,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
127
169
|
return 1;
|
|
128
170
|
}
|
|
129
171
|
const unsupported = unsupportedKinds(services);
|
|
130
|
-
if (unsupported.length > 0) {
|
|
172
|
+
if (phase !== 'down' && unsupported.length > 0) {
|
|
131
173
|
io.err(`managed infrastructure ${phase} failed: local execution capacity is unavailable (no container runtime, and the containerless backing cannot serve: ${unsupported.join(', ')})\n`);
|
|
132
174
|
return 1;
|
|
133
175
|
}
|
|
@@ -137,7 +179,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
137
179
|
await pgliteUp(services, worldData, launch);
|
|
138
180
|
}
|
|
139
181
|
catch (error) {
|
|
140
|
-
io.err(`managed infrastructure up failed: ${
|
|
182
|
+
io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
|
|
141
183
|
return 1;
|
|
142
184
|
}
|
|
143
185
|
io.out('managed infrastructure ready\n');
|
|
@@ -166,7 +208,30 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
166
208
|
const twinCli = redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)]);
|
|
167
209
|
const twinned = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || twinCli === undefined ? [] : declared.filter((s) => s.kind === 'redis');
|
|
168
210
|
/** The services the backing below answers for: every one, or those the twin does not serve (by compose service name). */
|
|
169
|
-
const
|
|
211
|
+
const native = declared.filter((s) => s.kind === 'mongodb');
|
|
212
|
+
const composed = twinned.length === 0 && native.length === 0 ? null : declared.filter((s) => !twinned.includes(s) && !native.includes(s)).map((s) => s.kind);
|
|
213
|
+
// A database's native wire and storage are its vendor server's. MongoDB never needs a Docker probe or container.
|
|
214
|
+
if (native.length > 0) {
|
|
215
|
+
const { pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
|
|
216
|
+
try {
|
|
217
|
+
if (phase === 'up') {
|
|
218
|
+
io.out('managed infrastructure backing: MongoDB server (containerless)\n');
|
|
219
|
+
await pgliteUp(native, worldData, launch);
|
|
220
|
+
}
|
|
221
|
+
else if (phase === 'status') {
|
|
222
|
+
const status = await pgliteStatus(native, worldData);
|
|
223
|
+
if (!status.ok)
|
|
224
|
+
throw new Error(`${status.ready}/${native.length} MongoDB services are ready`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
await pgliteDown(native, worldData);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
io.err(`managed infrastructure ${phase} failed: ${diagnostic(errorMessage(error))}\n`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
170
235
|
if (twinned.length > 0) {
|
|
171
236
|
const { redisTwinDown, redisTwinStatus, redisTwinUp } = await import("./redis-backing.js");
|
|
172
237
|
const cli = twinCli;
|
|
@@ -176,7 +241,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
176
241
|
await redisTwinUp(twinned, worldData, cli, launch);
|
|
177
242
|
}
|
|
178
243
|
catch (error) {
|
|
179
|
-
io.err(`managed infrastructure up failed: ${
|
|
244
|
+
io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
|
|
180
245
|
return 1;
|
|
181
246
|
}
|
|
182
247
|
}
|
|
@@ -190,21 +255,66 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
190
255
|
else {
|
|
191
256
|
await redisTwinDown(twinned, worldData);
|
|
192
257
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
258
|
+
}
|
|
259
|
+
if (composed?.length === 0) {
|
|
260
|
+
if (phase === 'up')
|
|
261
|
+
io.out('managed infrastructure ready\n');
|
|
262
|
+
else if (phase === 'status')
|
|
263
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
|
|
264
|
+
else
|
|
265
|
+
io.out('managed infrastructure stopped\n');
|
|
266
|
+
return 0;
|
|
202
267
|
}
|
|
203
268
|
const backing = await selectBacking();
|
|
204
269
|
if (typeof backing === 'number')
|
|
205
270
|
return backing;
|
|
206
271
|
if (backing === 'pglite')
|
|
207
272
|
return runPglite();
|
|
273
|
+
if (backing === 'native') {
|
|
274
|
+
const { nativePostgresUp, nativePostgresStatus, nativePostgresDown } = await import("./native-postgres-backing.js");
|
|
275
|
+
const postgres = declared.filter(s => s.kind === 'postgres');
|
|
276
|
+
const others = declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s));
|
|
277
|
+
const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
|
|
278
|
+
if (postgres.length !== 1 || unsupportedKinds(others).length) {
|
|
279
|
+
io.err('managed infrastructure native backing requires one postgres and only supported containerless services alongside it\n');
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
if (phase === 'up') {
|
|
284
|
+
io.out('managed infrastructure backing: native PostgreSQL (multi-session)\n');
|
|
285
|
+
await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
|
|
286
|
+
try {
|
|
287
|
+
await pgliteUp(others, worldData, launch);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
try {
|
|
291
|
+
await nativePostgresDown(worldData);
|
|
292
|
+
}
|
|
293
|
+
catch (cleanup) {
|
|
294
|
+
throw new AggregateError([error, cleanup], 'managed infrastructure startup and native cleanup failed');
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
io.out('managed infrastructure ready\n');
|
|
299
|
+
}
|
|
300
|
+
else if (phase === 'status') {
|
|
301
|
+
const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
|
|
302
|
+
if (!ready || !await nativePostgresStatus(worldData))
|
|
303
|
+
return 1;
|
|
304
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
await pgliteDown(others, worldData);
|
|
308
|
+
await nativePostgresDown(worldData);
|
|
309
|
+
io.out('managed infrastructure stopped\n');
|
|
310
|
+
}
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
io.err(`managed infrastructure ${phase} failed: ${diagnostic(errorMessage(error))}\n`);
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
208
318
|
const only = composed ?? [];
|
|
209
319
|
if (phase === 'up') {
|
|
210
320
|
const result = await run(['up', '-d', '--wait', ...only]);
|
|
@@ -222,10 +332,10 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
222
332
|
const expectedNames = expected.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
|
|
223
333
|
const runningNames = running.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
|
|
224
334
|
if (expectedNames.length === 0 || expectedNames.join('\0') !== runningNames.join('\0')) {
|
|
225
|
-
io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length}/${expectedNames.length + twinned.length} declared services are ready\n`);
|
|
335
|
+
io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length + native.length}/${expectedNames.length + twinned.length + native.length} declared services are ready\n`);
|
|
226
336
|
return 1;
|
|
227
337
|
}
|
|
228
|
-
io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length, connections: infraConnections(declared, env) })}\n`);
|
|
338
|
+
io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env) })}\n`);
|
|
229
339
|
}
|
|
230
340
|
else {
|
|
231
341
|
const result = await run(['down', '--remove-orphans']);
|
package/dist/src/init.js
CHANGED
|
@@ -45,13 +45,14 @@ import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
|
|
|
45
45
|
import { VENDOR_SERVER_BACKINGS } from "./backings.js";
|
|
46
46
|
import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
|
|
47
47
|
import { loadWorldConfig } from "./configs.js";
|
|
48
|
-
|
|
48
|
+
import { ownedPath } from "./owned-path.js";
|
|
49
|
+
/** Credential artifacts (files or directories) hold paths, never invented credential values. */
|
|
49
50
|
function fileCredentialNames() {
|
|
50
51
|
const out = new Set();
|
|
51
52
|
try {
|
|
52
53
|
for (const facts of Object.values(packFacts()))
|
|
53
54
|
for (const [name, spec] of Object.entries(facts?.credentialDoor?.fill ?? {}))
|
|
54
|
-
if (String(spec)
|
|
55
|
+
if (/^(file|directory):/.test(String(spec)))
|
|
55
56
|
out.add(name);
|
|
56
57
|
}
|
|
57
58
|
catch { /* no artifact: no file doors known */ }
|
|
@@ -297,8 +298,8 @@ const COMPOSE_INFRA = {
|
|
|
297
298
|
volumePath: '/data',
|
|
298
299
|
healthcheck: () => ['CMD', 'redis-cli', 'ping'],
|
|
299
300
|
},
|
|
300
|
-
//
|
|
301
|
-
//
|
|
301
|
+
// The runtime serves this kind with MongoDB's own mongod on PATH, without a container,
|
|
302
|
+
// at the same loopback port and URL; command: ["mongod", "--replSet", "world"] enables transactions.
|
|
302
303
|
mongodb: {
|
|
303
304
|
image: 'mongo:7',
|
|
304
305
|
containerPort: 27017,
|
|
@@ -528,10 +529,11 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
528
529
|
catch {
|
|
529
530
|
return {};
|
|
530
531
|
} })();
|
|
531
|
-
//
|
|
532
|
-
//
|
|
532
|
+
// A pack's starters, copied VISIBLY into the world dir: the author's to edit or delete (copy = ownership; never
|
|
533
|
+
// baked into the pack). Its handlers starter is the answering turns its own life scripts (journeys/scenario.json, the
|
|
534
|
+
// World's handler format, its faults left out); its default data is src/semantics/seed.ts.
|
|
533
535
|
const defaultsCopies = [];
|
|
534
|
-
const signals = options.vendors ? null : detectRepoVendors(repo);
|
|
536
|
+
const signals = options.vendors ? null : detectRepoVendors(repo, existing?.services);
|
|
535
537
|
const detected = options.vendors
|
|
536
538
|
? new Map(options.vendors.map((v) => [v, ['--twins']]))
|
|
537
539
|
: new Map([...signals.detected].filter(([vendor]) => (signals.uses.get(vendor) ?? []).some((use) => worldSelectionIncludes(selection, vendor, use.usage))));
|
|
@@ -554,6 +556,8 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
554
556
|
claimedEnv.set(name, service.id);
|
|
555
557
|
}
|
|
556
558
|
for (const vendor of [...detected.keys()].sort()) {
|
|
559
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(vendor))
|
|
560
|
+
throw new Error('Invalid service id');
|
|
557
561
|
const detectedVia = [...(detected.get(vendor) ?? [])].sort();
|
|
558
562
|
const backing = catalog.has(vendor) ? undefined : VENDOR_SERVER_BACKINGS[vendor];
|
|
559
563
|
if (backing !== undefined) {
|
|
@@ -593,28 +597,33 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
593
597
|
: `${wiring.note} CONFLICT: ${wiring.injectEnv} is already claimed by the "${conflict}" service, so this twin is emitted `
|
|
594
598
|
+ `without it — one world cannot point ${wiring.injectEnv} at two twins. Decide which vendor that var serves, or run them in separate worlds.`;
|
|
595
599
|
const packDir = catalogSource.packDir(vendor);
|
|
596
|
-
|
|
597
|
-
|
|
600
|
+
// the starter is the life's handlers that answer; its faults (an overload, a dropped turn) are the life's own tests and
|
|
601
|
+
// would fail a new World's first calls, so they stay behind
|
|
602
|
+
const starterHandlers = (() => {
|
|
603
|
+
try {
|
|
604
|
+
const doc = JSON.parse(readFileSync(join(packDir, 'journeys', 'scenario.json'), 'utf8'));
|
|
605
|
+
const answering = (doc.handlers ?? []).filter((h) => h.fault === undefined);
|
|
606
|
+
return answering.length ? `${JSON.stringify({ ...doc, handlers: answering }, null, 2)}\n` : null;
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
})();
|
|
612
|
+
const handlersTo = starterHandlers !== null ? ownedPath(out, `handlers/${vendor}.json`) : null;
|
|
598
613
|
if (handlersTo !== null)
|
|
599
|
-
defaultsCopies.push({ vendor, kind: 'handlers',
|
|
614
|
+
defaultsCopies.push({ vendor, kind: 'handlers', content: starterHandlers, to: handlersTo });
|
|
600
615
|
const handlersArg = handlersTo === null ? null : carried(handlersTo);
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
// beside a runner that makes each call against the twin, both the author's from then on
|
|
616
|
+
// a pack's default data (src/semantics/seed.ts: calls to the vendor's own operations, as data) is copied beside a
|
|
617
|
+
// runner that makes each call against the twin, both the author's from then on
|
|
604
618
|
const semanticsSeedPath = join(packDir, 'src', 'semantics', 'seed.ts');
|
|
605
|
-
if (existsSync(
|
|
606
|
-
defaultsCopies.push({ vendor, kind: 'seed', from:
|
|
607
|
-
|
|
608
|
-
defaultsCopies.push({ vendor, kind: 'seed-data', from: semanticsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.data.ts`) });
|
|
609
|
-
defaultsCopies.push({ vendor, kind: 'seed', content: seedCallsRunner(vendor), to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
|
|
619
|
+
if (existsSync(semanticsSeedPath)) {
|
|
620
|
+
defaultsCopies.push({ vendor, kind: 'seed-data', from: semanticsSeedPath, to: ownedPath(out, `seeds/defaults/${vendor}.data.ts`) });
|
|
621
|
+
defaultsCopies.push({ vendor, kind: 'seed', content: seedCallsRunner(vendor), to: ownedPath(out, `seeds/defaults/${vendor}.ts`) });
|
|
610
622
|
}
|
|
611
623
|
// COLOCATION (runtime contract R2a): a pack whose serve factory is known (derived into the
|
|
612
624
|
// pack-facts artifact) is emitted with `colocate`, so `up` boots it inside the ONE
|
|
613
625
|
// volter-world-host process instead of spawning a process per twin. The spawn `command`
|
|
614
|
-
// stays alongside as the process-isolation path. A service carrying a
|
|
615
|
-
// keeps the spawn path for now: the colocate factory contract has no scenarioPath yet,
|
|
616
|
-
// and silently dropping the world's handlers would be a fake success.
|
|
617
|
-
// A service carrying a handlers file colocates too (R2a): the colocate contract carries
|
|
626
|
+
// stays alongside as the process-isolation path. A service carrying a handlers file colocates too (R2a): the colocate contract carries
|
|
618
627
|
// `scenarioPath` and the host hands it to the factory — the same file the spawn path passes
|
|
619
628
|
// as --scenario.
|
|
620
629
|
const serveExport = packFacts()[vendor]?.serveExport;
|
|
@@ -710,7 +719,7 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
710
719
|
...services.filter((service) => !existingIds.has(service.id)),
|
|
711
720
|
].flatMap(endpointNames));
|
|
712
721
|
// Every name a twin of this World issues (its pack's credentialDoor fill), by the twin: the pack names them, so a name
|
|
713
|
-
// is issued whatever its stem (
|
|
722
|
+
// is issued whatever its stem (Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
|
|
714
723
|
// that vendor's twin is in the World, and never when it is not.
|
|
715
724
|
const issuedBy = new Map();
|
|
716
725
|
for (const service of [...(existing?.services ?? []).map(withIssues), ...services.filter((service) => !existingIds.has(service.id))]) {
|
|
@@ -1098,6 +1107,9 @@ export function writeWorldInit(plan, options = {}) {
|
|
|
1098
1107
|
throw new Error(`volter init: a world already exists at ${plan.configPath}. `
|
|
1099
1108
|
+ 'Pass --force to regenerate it, or --out <dir> to emit elsewhere.');
|
|
1100
1109
|
}
|
|
1110
|
+
// Recheck the planned name-derived paths before any output is written.
|
|
1111
|
+
for (const copy of plan.defaultsCopies)
|
|
1112
|
+
ownedPath(plan.out, relative(plan.out, copy.to));
|
|
1101
1113
|
mkdirSync(plan.out, { recursive: true });
|
|
1102
1114
|
writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
|
|
1103
1115
|
if (plan.inRepo)
|
|
@@ -1138,6 +1150,7 @@ export function seedCallsRunner(vendor) {
|
|
|
1138
1150
|
const stem = vendor.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
1139
1151
|
return `// ${vendor}'s default data, as its twin's pack declares it (./${vendor}.data.ts: calls to the vendor's own
|
|
1140
1152
|
// operations), made against the twin in order. Ownership transferred on copy: both files are yours — edit or delete.
|
|
1153
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
1141
1154
|
import { seed as calls } from './${vendor}.data.ts';
|
|
1142
1155
|
import * as defaults from './${vendor}.data.ts';
|
|
1143
1156
|
import type { SeedBrowser } from '@volter/world-core/browser';
|
|
@@ -1147,6 +1160,29 @@ export async function seed(): Promise<void> {
|
|
|
1147
1160
|
if ((process.env.VOLTER_SEED_SKIP ?? '').split(',').includes('${vendor}')) return;
|
|
1148
1161
|
const base = process.env.${stem}_TWIN_URL;
|
|
1149
1162
|
if (!base) throw new Error('${stem}_TWIN_URL is not set — bring the world up and run the seed with its env (volter-world attach/run)');
|
|
1163
|
+
<<<<<<< HEAD
|
|
1164
|
+
// a call dated before the World's time (\`at\`: state the account made then, through the API as it was) is made with
|
|
1165
|
+
// the World's clock frozen at that instant, and the clock is put back as it was found
|
|
1166
|
+
const clock = process.env.TWIN_WORLD_CLOCK_FILE;
|
|
1167
|
+
const found = clock && existsSync(clock) ? readFileSync(clock, 'utf8') : undefined;
|
|
1168
|
+
try {
|
|
1169
|
+
for (const call of calls) {
|
|
1170
|
+
if (call.at !== undefined) {
|
|
1171
|
+
if (!clock) throw new Error('${vendor} seed: a dated call needs the World clock (TWIN_WORLD_CLOCK_FILE): run the seed attached to the World');
|
|
1172
|
+
writeFileSync(clock, \`\${new Date(call.at).toISOString()}\n\`);
|
|
1173
|
+
}
|
|
1174
|
+
const res = await fetch(\`\${base.replace(/\\/$/, '')}\${call.path}\`, {
|
|
1175
|
+
method: call.method,
|
|
1176
|
+
headers: { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) },
|
|
1177
|
+
...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
|
|
1178
|
+
});
|
|
1179
|
+
if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
|
|
1180
|
+
}
|
|
1181
|
+
} finally {
|
|
1182
|
+
if (clock && calls.some((call) => call.at !== undefined)) {
|
|
1183
|
+
if (found === undefined) rmSync(clock, { force: true }); else writeFileSync(clock, found);
|
|
1184
|
+
}
|
|
1185
|
+
=======
|
|
1150
1186
|
const clients = new Map<string, SeedBrowser>();
|
|
1151
1187
|
const snapshots = new Map<string, Awaited<ReturnType<SeedBrowser['state']>>>();
|
|
1152
1188
|
let browserHelper: typeof import('@volter/world-core/browser') | undefined;
|
|
@@ -1183,6 +1219,7 @@ export async function seed(): Promise<void> {
|
|
|
1183
1219
|
headers, ...(body === undefined ? {} : { body }),
|
|
1184
1220
|
});
|
|
1185
1221
|
if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
|
|
1222
|
+
>>>>>>> origin/main
|
|
1186
1223
|
}
|
|
1187
1224
|
}
|
|
1188
1225
|
`;
|
|
@@ -22,7 +22,9 @@ export declare function handoffWorldLifecycle(root: string, world: string, pids:
|
|
|
22
22
|
export declare function retainWorldLifecycleForCleanup(root: string, world: string): void;
|
|
23
23
|
export declare function recordWorldLifecycleEvent(root: string, world: string, message: string): void;
|
|
24
24
|
/** Called only after verified cleanup. Never touches legacy records. */
|
|
25
|
-
export declare function releaseWorldLifecycle(root: string, world: string): void;
|
|
25
|
+
export declare function releaseWorldLifecycle(root: string, world: string, receipt?: string): void;
|
|
26
|
+
/** Explicit surrender must prove this generation's complete local inventory has exited. */
|
|
27
|
+
export declare function exitedWorldLifecyclePids(root: string, world: string, createdAt: string): number[] | undefined;
|
|
26
28
|
/**
|
|
27
29
|
* Releases the record of a World whose instance metadata is gone when nothing it
|
|
28
30
|
* recorded can still run: the record is this host's and this user's, and its owner
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Durable ownership of one World. This inventory does not allocate or reserve capacity.
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
import { volterHome } from '@volter/world-core/runtime';
|
|
5
5
|
import { hostname } from 'node:os';
|
|
@@ -7,10 +7,19 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
7
7
|
import { stateDirName, withFileLock } from '@volter/world-core/runtime';
|
|
8
8
|
import { checkedOwner } from "./consumers.js";
|
|
9
9
|
const directory = () => join(volterHome(), 'world-lifecycles');
|
|
10
|
-
|
|
10
|
+
/** One directory is one root however it is spelled: macOS's /tmp is /private/tmp, and a caller's cwd is the real path
|
|
11
|
+
* while its argument may be the alias. An identity hashed from the spelling made doctor report a running World's
|
|
12
|
+
* ownership missing when the World was started under /tmp and inspected from /private/tmp. */
|
|
13
|
+
const canonical = (root) => { try {
|
|
14
|
+
return realpathSync(root);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return resolve(root);
|
|
18
|
+
} };
|
|
19
|
+
const identity = (root, world) => createHash('sha256').update(`${canonical(root)}\0${world}`).digest('hex');
|
|
11
20
|
const recordPath = (root, world) => join(directory(), `${identity(root, world)}.json`);
|
|
12
21
|
const legacyPath = (root, world) => join(volterHome(), 'world-resource-claims', `${identity(root, world)}.json`);
|
|
13
|
-
export const lifecycleLogPath = (root, world) => join(
|
|
22
|
+
export const lifecycleLogPath = (root, world) => join(canonical(root), stateDirName(), 'worlds', '.lifecycle', `${world}.log`);
|
|
14
23
|
function read(path) {
|
|
15
24
|
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
16
25
|
if (record?.version !== 1 || typeof record.world !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(record.world)
|
|
@@ -47,7 +56,7 @@ export function beginWorldLifecycle(root, world, instanceCreatedAt, owner) {
|
|
|
47
56
|
checkedOwner(owner);
|
|
48
57
|
assertLifecycleStartable(root, world);
|
|
49
58
|
const record = {
|
|
50
|
-
version: 1, world, root:
|
|
59
|
+
version: 1, world, root: canonical(root), identity: identity(root, world), instanceCreatedAt,
|
|
51
60
|
owner, ownerUid: process.getuid?.(), ownerPid: process.pid, hostname: hostname(),
|
|
52
61
|
startedAt: new Date().toISOString(), phase: 'starting', pids: [], log: lifecycleLogPath(root, world),
|
|
53
62
|
};
|
|
@@ -81,7 +90,7 @@ export function recordWorldLifecycleEvent(root, world, message) {
|
|
|
81
90
|
writeFileSync(path, `${new Date().toISOString()} ${message}\n`, { flag: 'a', mode: 0o600 });
|
|
82
91
|
}
|
|
83
92
|
/** Called only after verified cleanup. Never touches legacy records. */
|
|
84
|
-
export function releaseWorldLifecycle(root, world) {
|
|
93
|
+
export function releaseWorldLifecycle(root, world, receipt = 'teardown verified; ownership released') {
|
|
85
94
|
const path = recordPath(root, world);
|
|
86
95
|
if (!existsSync(path))
|
|
87
96
|
return;
|
|
@@ -94,12 +103,26 @@ export function releaseWorldLifecycle(root, world) {
|
|
|
94
103
|
// The receipt line is evidence, not a precondition: failing to append it must not keep ownership (and so the next
|
|
95
104
|
// `up`) blocked after a verified teardown.
|
|
96
105
|
try {
|
|
97
|
-
recordWorldLifecycleEvent(root, world,
|
|
106
|
+
recordWorldLifecycleEvent(root, world, receipt);
|
|
98
107
|
}
|
|
99
108
|
catch { /* released regardless */ }
|
|
100
109
|
rmSync(path);
|
|
101
110
|
});
|
|
102
111
|
}
|
|
112
|
+
/** Explicit surrender must prove this generation's complete local inventory has exited. */
|
|
113
|
+
export function exitedWorldLifecyclePids(root, world, createdAt) {
|
|
114
|
+
try {
|
|
115
|
+
const record = read(recordPath(root, world));
|
|
116
|
+
if (record.hostname !== hostname() || record.instanceCreatedAt !== createdAt
|
|
117
|
+
|| (record.ownerUid !== undefined && record.ownerUid !== process.getuid?.()))
|
|
118
|
+
return undefined;
|
|
119
|
+
const pids = [...new Set([record.ownerPid, ...record.pids])];
|
|
120
|
+
return pids.some(processExists) ? undefined : pids;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
103
126
|
/**
|
|
104
127
|
* Releases the record of a World whose instance metadata is gone when nothing it
|
|
105
128
|
* recorded can still run: the record is this host's and this user's, and its owner
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { signalChild } from './process-signal.mjs';
|
|
2
|
+
import { errorMessage } from './error-message.mjs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
|
|
6
|
+
if (!process.env.VOLTER_WORLD) throw new Error('LiveKit backing must run as a World service');
|
|
7
|
+
const port = Number(process.env.PORT);
|
|
8
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Missing allocated service port');
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
let supplied = {};
|
|
11
|
+
for (let index = 0; index < args.length; index++) {
|
|
12
|
+
if (args[index] !== '--config-body') continue;
|
|
13
|
+
const value = parse(args[index + 1]);
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid LiveKit config body');
|
|
15
|
+
supplied = { ...supplied, ...value };
|
|
16
|
+
args.splice(index, 2); index--;
|
|
17
|
+
}
|
|
18
|
+
// LiveKit's embedded TURN denies restricted peer addresses by default. This relay serves only this local SFU.
|
|
19
|
+
// Source: https://github.com/livekit/livekit/blob/v1.13.1/pkg/config/config.go (TURNConfig).
|
|
20
|
+
const config = {
|
|
21
|
+
...supplied, port, bind_addresses: ['127.0.0.1'],
|
|
22
|
+
rtc: { ...supplied.rtc, node_ip: '127.0.0.1', tcp_port: 0 },
|
|
23
|
+
turn: { ...supplied.turn, enabled: true, domain: '127.0.0.1', udp_port: port, bind_addresses: ['127.0.0.1'],
|
|
24
|
+
allow_restricted_peer_cidrs: ['127.0.0.0/8'] },
|
|
25
|
+
};
|
|
26
|
+
const child = spawn('livekit-server', ['--config-body', JSON.stringify(config), ...args], {
|
|
27
|
+
stdio: 'inherit', env: process.env,
|
|
28
|
+
});
|
|
29
|
+
let stopping = false;
|
|
30
|
+
const stop = () => {
|
|
31
|
+
stopping = true;
|
|
32
|
+
try { if (child.pid) signalChild(child, 'SIGTERM'); }
|
|
33
|
+
catch (error) { console.error(errorMessage(error)); process.exitCode = 1; }
|
|
34
|
+
};
|
|
35
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
36
|
+
child.once('error', error => { console.error(errorMessage(error)); process.exitCode = 1; });
|
|
37
|
+
child.once('exit', (code, signal) => { process.exitCode = stopping ? 0 : code ?? (signal ? 1 : 0); });
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// parent through its own doors (its history cut at an instant, when one is asked), its clock frozen at
|
|
4
4
|
// that instant so no twin's catch-up walks it forward, and removed when its time runs out. world-host
|
|
5
5
|
// and `volter world view` both make them with this; a hosted World's supervisor makes its own.
|
|
6
|
+
import { errorMessage } from "./error-message.js";
|
|
6
7
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
8
|
import { dirname, join } from 'node:path';
|
|
8
9
|
import { readBranchMeta, stateDirName, withHistoryLock, withStateRemoval, worldPaths, worldStateRoot, writeBranchMeta } from '@volter/world-core/runtime';
|
|
@@ -202,8 +203,15 @@ export class LocalBranches {
|
|
|
202
203
|
try {
|
|
203
204
|
await mounted?.stop();
|
|
204
205
|
}
|
|
205
|
-
catch {
|
|
206
|
-
|
|
206
|
+
catch (cleanup) {
|
|
207
|
+
throw new AggregateError([error, cleanup], 'Branch startup and host cleanup failed; state retained');
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
withStateRemoval(root, () => rmSync(root, { recursive: true, force: true }));
|
|
211
|
+
}
|
|
212
|
+
catch (cleanup) {
|
|
213
|
+
throw new AggregateError([error, cleanup], 'Branch startup and state cleanup failed');
|
|
214
|
+
}
|
|
207
215
|
throw error;
|
|
208
216
|
}
|
|
209
217
|
}
|
|
@@ -250,7 +258,7 @@ export class LocalBranches {
|
|
|
250
258
|
await this.remove(name);
|
|
251
259
|
}
|
|
252
260
|
catch (error) {
|
|
253
|
-
this.o.announce?.(`branch ${name}: removal failed: ${
|
|
261
|
+
this.o.announce?.(`branch ${name}: removal failed: ${errorMessage(error)}`);
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HostLaunch, InfraService } from './pglite-backing.js';
|
|
2
|
+
/** PATH is the binary contract, as for the native PostgreSQL example and LiveKit. No acquisition at boot. */
|
|
3
|
+
export declare function mongodbArgv(service: InfraService, data: string): string[];
|
|
4
|
+
/** MongoDB's structured record comes from this child, not a listener already holding the port. */
|
|
5
|
+
export declare function mongodbListening(log: string, port: number): boolean;
|
|
6
|
+
/** Ping the server; initialize only an explicitly declared one-member set and await its primary. */
|
|
7
|
+
export declare function mongodbReady(service: InfraService, launch: HostLaunch, deadline: number, initialize?: boolean): Promise<void>;
|