@volter/world-runtime 3.0.37 → 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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// The containerless backing for World-managed infrastructure, instead of docker
|
|
2
2
|
// compose: a POSTGRES service is a PGlite host (see pglite-host.mjs), a MONGODB
|
|
3
|
-
// service is
|
|
4
|
-
//
|
|
5
|
-
// runtime is available (or by explicit VOLTER_WORLD_INFRA_BACKING=pglite); the
|
|
3
|
+
// service is MongoDB's own mongod (mongodb-backing.ts). MongoDB is always native; Postgres is selected
|
|
4
|
+
// when no container runtime is available (or by VOLTER_WORLD_INFRA_BACKING=pglite). The
|
|
6
5
|
// world config, definition file, injected env, and declared-service contract are
|
|
7
6
|
// byte-identical either way — backing is private, exactly as the boundary comment
|
|
8
7
|
// in infra-cli.ts promises. Each service's bytes live under VOLTER_WORLD_DATA, so
|
|
@@ -12,39 +11,40 @@
|
|
|
12
11
|
// services only. A definition declaring mysql without a container runtime is
|
|
13
12
|
// refused with the kinds named, never half-booted. Redis never reaches it: every
|
|
14
13
|
// redis service is the redis twin's (redis-backing.ts).
|
|
15
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync
|
|
14
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
16
15
|
import { connect } from 'node:net';
|
|
17
16
|
import { dirname, join } from 'node:path';
|
|
18
|
-
import { spawn
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import { commandLifetime, COMMAND_GRACE_MS, COMMAND_CONFIRM_MS } from "./command-lifetime.js";
|
|
19
|
+
import { survivingOwnedGroups } from "./process-groups.js";
|
|
20
|
+
import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess } from "./process-receipt.js";
|
|
21
|
+
import { errorMessage } from "./error-message.js";
|
|
19
22
|
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
* reads exactly that shape back (service key at 2-space indent = the kind;
|
|
25
|
-
* the single loopback port mapping beneath it). Anything else is a malformed
|
|
26
|
-
* definition and fails loudly. */
|
|
23
|
+
import { parse } from 'yaml';
|
|
24
|
+
import { mongodbArgv, mongodbListening, mongodbReady } from "./mongodb-backing.js";
|
|
25
|
+
/** Read the generated definition's loopback port mappings. MongoDB may explicitly request
|
|
26
|
+
* one replica-set member through its Compose-compatible command; other mongod options are refused. */
|
|
27
27
|
export function parseInfraDefinition(text) {
|
|
28
|
+
const definition = parse(text);
|
|
28
29
|
const services = [];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (service) {
|
|
40
|
-
current = service[1];
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const port = line.match(/^ {6}- "127\.0\.0\.1:(\d+):\d+"\s*$/);
|
|
44
|
-
if (port && current) {
|
|
45
|
-
services.push({ kind: current, hostPort: Number(port[1]) });
|
|
46
|
-
current = null;
|
|
30
|
+
for (const [kind, value] of Object.entries(definition?.services ?? {})) {
|
|
31
|
+
const declared = value;
|
|
32
|
+
let replicaSet;
|
|
33
|
+
if (kind === 'mongodb' && declared.command !== undefined) {
|
|
34
|
+
const command = declared.command;
|
|
35
|
+
if (!Array.isArray(command) || command.length !== 3 || command[0] !== 'mongod' || command[1] !== '--replSet'
|
|
36
|
+
|| typeof command[2] !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(command[2])) {
|
|
37
|
+
throw new Error('World-managed MongoDB command must be ["mongod", "--replSet", "<name>"] or absent for standalone');
|
|
38
|
+
}
|
|
39
|
+
replicaSet = command[2];
|
|
47
40
|
}
|
|
41
|
+
const ports = (declared.ports ?? []).flatMap((port) => {
|
|
42
|
+
const match = typeof port === 'string' ? port.match(/^127\.0\.0\.1:(\d+):\d+$/) : null;
|
|
43
|
+
return match ? [Number(match[1])] : [];
|
|
44
|
+
});
|
|
45
|
+
if (ports.length !== 1)
|
|
46
|
+
throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port`);
|
|
47
|
+
services.push({ kind, hostPort: ports[0], ...(replicaSet ? { replicaSet } : {}) });
|
|
48
48
|
}
|
|
49
49
|
return services;
|
|
50
50
|
}
|
|
@@ -74,50 +74,50 @@ export function infraConnections(services, env) {
|
|
|
74
74
|
const here = () => fileURLToPath(new URL('.', import.meta.url));
|
|
75
75
|
const hostScript = () => join(here(), 'pglite-host.mjs');
|
|
76
76
|
const READY_TIMEOUT_MS = 60_000;
|
|
77
|
-
const MONGODB_TWIN = '@volter/twin-mongodb';
|
|
78
77
|
/** The kinds this backing serves, and the name its files carry under the data dir. */
|
|
79
|
-
const CONTAINERLESS = { postgres: 'pglite-postgres', mongodb: '
|
|
78
|
+
const CONTAINERLESS = { postgres: 'pglite-postgres', mongodb: 'mongod-mongodb' };
|
|
79
|
+
// Shared with stopped-state branching: only these backings own cold-copied database bytes.
|
|
80
|
+
export function containerlessDataDirectory(kind) {
|
|
81
|
+
const name = CONTAINERLESS[kind];
|
|
82
|
+
return name === undefined ? undefined : `${name}-data`;
|
|
83
|
+
}
|
|
80
84
|
function pidPath(dataDir, kind) {
|
|
81
85
|
return join(dataDir, `${CONTAINERLESS[kind] ?? `pglite-${kind}`}.pid`);
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!cli.endsWith('.ts') || underBun)
|
|
100
|
-
return process.execPath;
|
|
101
|
-
if (bunOnPath())
|
|
102
|
-
return 'bun';
|
|
103
|
-
throw new Error(`a checkout's MongoDB twin runs under bun, which is not on PATH (${cli} is TypeScript that Node cannot run)`);
|
|
87
|
+
const hosts = new Map();
|
|
88
|
+
function hostOwnership(pidFile) {
|
|
89
|
+
const receipt = readProcessReceipt(pidFile, text => {
|
|
90
|
+
if (!/^[1-9][0-9]*\s*$/.test(text))
|
|
91
|
+
throw new Error('Invalid managed host PID');
|
|
92
|
+
const pid = Number(text.trim());
|
|
93
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
94
|
+
throw new Error('Invalid managed host PID');
|
|
95
|
+
return pid;
|
|
96
|
+
});
|
|
97
|
+
const spawn = readSpawnReceipt(pidFile);
|
|
98
|
+
const errors = [receipt.error, spawn.error].filter((error) => !!error);
|
|
99
|
+
if (receipt.value && spawn.value && receipt.value !== spawn.value.pid) {
|
|
100
|
+
errors.push(new Error('Managed host receipts disagree; ownership uncertain'));
|
|
101
|
+
}
|
|
102
|
+
return { present: receipt.present || spawn.present, pid: spawn.value?.pid ?? receipt.value, identity: spawn.value, errors, recordedPid: receipt.value, readyReceipt: receipt.value !== undefined, spawnPresent: spawn.present };
|
|
104
103
|
}
|
|
105
104
|
/** The argv that serves one service on its declared loopback port, with its bytes in `serviceData`. */
|
|
106
|
-
function hostArgv(service, serviceData
|
|
107
|
-
if (service.kind === 'mongodb')
|
|
108
|
-
|
|
109
|
-
// blocking (spawnSync), as it was in the tool: reached only for a checkout's TypeScript cli under Node
|
|
110
|
-
return [twinRunner(cli, () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000, windowsHide: true, env }).status === 0), cli, 'serve', '--port', String(service.hostPort), '--root', serviceData];
|
|
111
|
-
}
|
|
105
|
+
function hostArgv(service, serviceData) {
|
|
106
|
+
if (service.kind === 'mongodb')
|
|
107
|
+
return mongodbArgv(service, serviceData);
|
|
112
108
|
return ['node', hostScript(), '--port', String(service.hostPort), '--data', serviceData];
|
|
113
109
|
}
|
|
114
110
|
function alive(pid) {
|
|
111
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
112
|
+
return false;
|
|
115
113
|
try {
|
|
116
114
|
process.kill(pid, 0);
|
|
117
115
|
return true;
|
|
118
116
|
}
|
|
119
|
-
catch {
|
|
120
|
-
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === 'ESRCH')
|
|
119
|
+
return false;
|
|
120
|
+
throw error;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
function listening(port) {
|
|
@@ -137,16 +137,11 @@ export function unsupportedKinds(services) {
|
|
|
137
137
|
}
|
|
138
138
|
/** The line each host prints once ITS OWN listener is bound: readiness is taken from the child, never
|
|
139
139
|
* from a connect to the port, which any other process holding the port would answer. */
|
|
140
|
-
function readyLine(
|
|
141
|
-
return
|
|
140
|
+
function readyLine(port) {
|
|
141
|
+
return `serving postgres wire protocol on 127.0.0.1:${port}`;
|
|
142
142
|
}
|
|
143
143
|
function logSince(log, offset) {
|
|
144
|
-
|
|
145
|
-
return readFileSync(log).subarray(offset).toString('utf8');
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return '';
|
|
149
|
-
}
|
|
144
|
+
return readFileSync(log).subarray(offset).toString('utf8');
|
|
150
145
|
}
|
|
151
146
|
export async function pgliteUp(services, dataDir, launch = {}) {
|
|
152
147
|
const env = launch.env ?? process.env;
|
|
@@ -154,15 +149,22 @@ export async function pgliteUp(services, dataDir, launch = {}) {
|
|
|
154
149
|
const plan = [];
|
|
155
150
|
for (const service of services) {
|
|
156
151
|
const pidFile = pidPath(dataDir, service.kind);
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
const ownership = hostOwnership(pidFile);
|
|
153
|
+
if (ownership.present || hosts.has(pidFile)) {
|
|
154
|
+
if (!ownership.errors.length && ownership.readyReceipt && ownership.pid && alive(ownership.pid)) {
|
|
155
|
+
if (ownership.identity)
|
|
156
|
+
await verifySpawnIdentity(ownership.identity);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
await stopHost(pidFile);
|
|
160
|
+
}
|
|
159
161
|
const name = CONTAINERLESS[service.kind] ?? `pglite-${service.kind}`;
|
|
160
|
-
const serviceData = join(dataDir, `${name}-data`);
|
|
162
|
+
const serviceData = join(dataDir, containerlessDataDirectory(service.kind) ?? `${name}-data`);
|
|
161
163
|
const log = join(dataDir, `${name}.log`);
|
|
162
164
|
if (await listening(service.hostPort)) {
|
|
163
165
|
throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
|
|
164
166
|
}
|
|
165
|
-
plan.push({ service, name, serviceData, log, pidFile, argv: hostArgv(service, serviceData
|
|
167
|
+
plan.push({ service, name, serviceData, log, pidFile, argv: hostArgv(service, serviceData) });
|
|
166
168
|
}
|
|
167
169
|
const started = [];
|
|
168
170
|
try {
|
|
@@ -179,15 +181,31 @@ export async function pgliteUp(services, dataDir, launch = {}) {
|
|
|
179
181
|
detached: true,
|
|
180
182
|
stdio: ['ignore', logFd, logFd],
|
|
181
183
|
});
|
|
182
|
-
child.
|
|
184
|
+
// Retain the actual child BEFORE descriptor close or either publication can fail.
|
|
185
|
+
const lifetime = commandLifetime(child, launch.signal);
|
|
186
|
+
const host = { child, lifetime, closed: false };
|
|
187
|
+
child.once('close', () => { host.closed = true; });
|
|
188
|
+
hosts.set(pidFile, host);
|
|
189
|
+
started.push(pidFile);
|
|
190
|
+
let spawnError;
|
|
191
|
+
child.on('error', (error) => { spawnError = error; });
|
|
183
192
|
closeSync(logFd);
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
if (!child.pid)
|
|
194
|
+
throw new Error(service.kind === 'mongodb'
|
|
195
|
+
? "World-managed MongoDB needs MongoDB's own mongod on PATH; see cookbook/mongodb-backing/README.md"
|
|
196
|
+
: `${name} could not start ${command}`);
|
|
197
|
+
host.identity = await lifetime.identity;
|
|
198
|
+
if (!host.identity?.start)
|
|
199
|
+
throw new Error('Managed host birth capture failed; retiring held child');
|
|
200
|
+
publishProcessReceipt(spawnReceiptPath(pidFile), JSON.stringify(host.identity) + '\n');
|
|
201
|
+
publishProcessReceipt(pidFile, `${child.pid}\n`);
|
|
186
202
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
187
|
-
const line = readyLine(service.
|
|
203
|
+
const line = readyLine(service.hostPort);
|
|
188
204
|
// ready when OUR child says its listener is bound (a squatter that took the port after the check
|
|
189
205
|
// above makes the child fail its bind and exit, never print this) and the port answers
|
|
190
|
-
while (!(alive(child.pid) && logSince(log, offset).includes(line) && await listening(service.hostPort))) {
|
|
206
|
+
while (!(alive(child.pid) && (service.kind === 'mongodb' ? mongodbListening(logSince(log, offset), service.hostPort) : logSince(log, offset).includes(line)) && await listening(service.hostPort))) {
|
|
207
|
+
if (spawnError)
|
|
208
|
+
throw spawnError;
|
|
191
209
|
if (!alive(child.pid))
|
|
192
210
|
throw new Error(`${name} exited before serving — see ${log}`);
|
|
193
211
|
if (Date.now() > deadline)
|
|
@@ -196,19 +214,24 @@ export async function pgliteUp(services, dataDir, launch = {}) {
|
|
|
196
214
|
launch.signal?.throwIfAborted();
|
|
197
215
|
await sleep(200);
|
|
198
216
|
}
|
|
217
|
+
if (service.kind === 'mongodb')
|
|
218
|
+
await mongodbReady(service, launch, deadline);
|
|
219
|
+
child.unref();
|
|
199
220
|
}
|
|
200
221
|
}
|
|
201
222
|
catch (error) {
|
|
202
223
|
// tear down what this up started, so a failed up leaves nothing running
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
const errors = [error];
|
|
225
|
+
for (const pidFile of started) {
|
|
226
|
+
try {
|
|
227
|
+
await stopHost(pidFile);
|
|
228
|
+
}
|
|
229
|
+
catch (cleanupError) {
|
|
230
|
+
errors.push(cleanupError);
|
|
209
231
|
}
|
|
210
|
-
rmSync(pidFile, { force: true });
|
|
211
232
|
}
|
|
233
|
+
if (errors.length > 1)
|
|
234
|
+
throw new AggregateError(errors, 'Managed infrastructure startup and cleanup failed');
|
|
212
235
|
throw error;
|
|
213
236
|
}
|
|
214
237
|
}
|
|
@@ -216,35 +239,104 @@ export async function pgliteStatus(services, dataDir) {
|
|
|
216
239
|
let ready = 0;
|
|
217
240
|
for (const service of services) {
|
|
218
241
|
const pidFile = pidPath(dataDir, service.kind);
|
|
219
|
-
|
|
242
|
+
const ownership = hostOwnership(pidFile);
|
|
243
|
+
if (ownership.errors.length)
|
|
244
|
+
throw new AggregateError(ownership.errors, 'Managed infrastructure ownership uncertain; retry down');
|
|
245
|
+
if (!ownership.readyReceipt || !ownership.pid || !alive(ownership.pid) || !(await listening(service.hostPort)))
|
|
220
246
|
continue;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
247
|
+
if (ownership.identity)
|
|
248
|
+
await verifySpawnIdentity(ownership.identity);
|
|
249
|
+
if (service.kind === 'mongodb') {
|
|
250
|
+
await mongodbReady(service, {}, Date.now() + READY_TIMEOUT_MS, false);
|
|
251
|
+
}
|
|
252
|
+
ready += 1;
|
|
224
253
|
}
|
|
225
254
|
return { ok: ready === services.length && services.length > 0, ready };
|
|
226
255
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
256
|
+
/** Retain evidence until both the child and its private group are confirmed stopped. */
|
|
257
|
+
async function stopHost(pidFile) {
|
|
258
|
+
const held = hosts.get(pidFile);
|
|
259
|
+
const ownership = hostOwnership(pidFile);
|
|
260
|
+
if (ownership.errors.length)
|
|
261
|
+
process.stderr.write(`${errorMessage(new AggregateError(ownership.errors, 'Managed host ownership uncertain; retrying established identity'))}\n`);
|
|
262
|
+
if (held) {
|
|
263
|
+
const pid = held.child.pid;
|
|
264
|
+
const retired = () => held.closed && (!pid || survivingOwnedGroups([pid]).length === 0);
|
|
265
|
+
// A failed lifetime promise is a past diagnosis. A later close and vanished
|
|
266
|
+
// owned group are current retirement evidence, even when finish stays rejected.
|
|
267
|
+
if (!retired()) {
|
|
268
|
+
held.child.ref();
|
|
269
|
+
held.lifetime.cancel('SIGTERM');
|
|
234
270
|
try {
|
|
235
|
-
|
|
271
|
+
await held.lifetime.finish();
|
|
236
272
|
}
|
|
237
|
-
catch {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
await sleep(100);
|
|
241
|
-
if (alive(pid)) {
|
|
242
|
-
try {
|
|
243
|
-
process.kill(pid, 'SIGKILL');
|
|
244
|
-
}
|
|
245
|
-
catch { /* raced */ }
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (!retired())
|
|
275
|
+
throw error;
|
|
246
276
|
}
|
|
247
277
|
}
|
|
278
|
+
}
|
|
279
|
+
else if (ownership.pid) {
|
|
280
|
+
const pid = ownership.pid;
|
|
281
|
+
const live = () => survivingOwnedGroups([pid]).length > 0;
|
|
282
|
+
if (live()) {
|
|
283
|
+
if (ownership.identity)
|
|
284
|
+
await verifySpawnIdentity(ownership.identity);
|
|
285
|
+
else
|
|
286
|
+
throw new AggregateError(ownership.errors, 'Managed host spawn identity missing or unreadable; ownership retained');
|
|
287
|
+
// Reuse commandLifetime's existing grace and confirmation budgets; no new limit.
|
|
288
|
+
for (const [signal, limit] of [['SIGTERM', COMMAND_GRACE_MS], ['SIGKILL', COMMAND_CONFIRM_MS]]) {
|
|
289
|
+
if (!live())
|
|
290
|
+
break;
|
|
291
|
+
await signalOwnedProcess(ownership.identity, signal, true);
|
|
292
|
+
const deadline = Date.now() + limit;
|
|
293
|
+
while (live() && Date.now() < deadline)
|
|
294
|
+
await sleep(20);
|
|
295
|
+
}
|
|
296
|
+
if (live())
|
|
297
|
+
throw new Error(`managed infrastructure group ${pid} did not stop; retained ${pidFile}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (ownership.present) {
|
|
301
|
+
throw new AggregateError(ownership.errors, 'Managed host ownership uncertain; no established identity; receipt retained');
|
|
302
|
+
}
|
|
303
|
+
if (ownership.recordedPid && survivingOwnedGroups([ownership.recordedPid]).length) {
|
|
304
|
+
throw new AggregateError(ownership.errors, 'Managed host receipt still names a live or uncertain group; ownership retained');
|
|
305
|
+
}
|
|
306
|
+
const failures = [];
|
|
307
|
+
try {
|
|
248
308
|
rmSync(pidFile, { force: true });
|
|
249
309
|
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
failures.push(error);
|
|
312
|
+
}
|
|
313
|
+
// A later CLI must still have this identity if clearing the damaged PID receipt failed.
|
|
314
|
+
if (!failures.length) {
|
|
315
|
+
try {
|
|
316
|
+
rmSync(spawnReceiptPath(pidFile), { force: true });
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
failures.push(error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (failures.length)
|
|
323
|
+
throw new AggregateError(failures, 'Managed host stopped but receipt cleanup failed');
|
|
324
|
+
hosts.delete(pidFile);
|
|
325
|
+
}
|
|
326
|
+
export async function pgliteDown(services, dataDir) {
|
|
327
|
+
const failures = [];
|
|
328
|
+
for (const service of services) {
|
|
329
|
+
try {
|
|
330
|
+
const pidFile = pidPath(dataDir, service.kind);
|
|
331
|
+
if (unsupportedKinds([service]).length && (hostOwnership(pidFile).present || hosts.has(pidFile))) {
|
|
332
|
+
throw new Error(`cannot stop recorded ${service.kind} infrastructure with this backing; use its owning runtime`);
|
|
333
|
+
}
|
|
334
|
+
await stopHost(pidFile);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
failures.push(error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (failures.length)
|
|
341
|
+
throw new AggregateError(failures, 'Managed infrastructure retirement failed');
|
|
250
342
|
}
|
package/dist/src/pglite-host.mjs
CHANGED
|
@@ -84,12 +84,37 @@
|
|
|
84
84
|
// pg_try_advisory_lock on a held key returns true); a NOTIFY is delivered
|
|
85
85
|
// to the connection that sent it, not to the one that ran LISTEN.
|
|
86
86
|
// CREATE ROLE succeeds but connections never run as that role.
|
|
87
|
+
//
|
|
88
|
+
// Database timestamps use the World clock: PGlite reads Date.now() for now(),
|
|
89
|
+
// CURRENT_TIMESTAMP, clock_timestamp() and transaction starts. Refresh the file
|
|
90
|
+
// at each connection message; without a file the host uses machine time.
|
|
91
|
+
// Gateway timers retain machineNow. A frozen database clock also freezes
|
|
92
|
+
// pg_sleep; elapsed-time SQL and timeouts are not promised in that mode.
|
|
93
|
+
// Randomness remains the host's own; no database schema objects are installed.
|
|
94
|
+
import { errorMessage } from './error-message.mjs';
|
|
87
95
|
import net from 'node:net';
|
|
88
|
-
import { mkdirSync } from 'node:fs';
|
|
89
|
-
import
|
|
90
|
-
import { vector } from '@electric-sql/pglite-pgvector';
|
|
96
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
97
|
+
import clockForm from '@volter/world-core/world-clock';
|
|
91
98
|
import { fromNodeSocket } from 'pg-gateway/node';
|
|
92
99
|
|
|
100
|
+
// ---- the World's time (above) --------------------------------
|
|
101
|
+
const machineNow = Date.now.bind(Date);
|
|
102
|
+
const clockFile = process.env.TWIN_WORLD_CLOCK_FILE;
|
|
103
|
+
let worldClock = null;
|
|
104
|
+
/** Read the World clock again: at each message a connection sends. */
|
|
105
|
+
function readWorldClock() {
|
|
106
|
+
if (!clockFile) return;
|
|
107
|
+
try {
|
|
108
|
+
worldClock = clockForm.parseClock(readFileSync(clockFile, 'utf8'));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// no clock: the machine's time; a clock caught mid-write keeps the last one read until the next message
|
|
111
|
+
if (error?.code === 'ENOENT') worldClock = null;
|
|
112
|
+
else process.stderr.write(`pglite-host: clock read failed: ${errorMessage(error)}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
readWorldClock();
|
|
116
|
+
Date.now = () => (worldClock ? clockForm.clockNowMs(worldClock, machineNow()) : machineNow());
|
|
117
|
+
|
|
93
118
|
// The contrib extensions shipped in @electric-sql/pglite/dist/contrib (the
|
|
94
119
|
// module name; `uuid_ossp` installs as "uuid-ossp"). auto_explain is omitted:
|
|
95
120
|
// it is a preload library with no CREATE EXTENSION control file.
|
|
@@ -101,6 +126,8 @@ const CONTRIB = [
|
|
|
101
126
|
'pg_trgm', 'pg_visibility', 'pg_walinspect', 'pgcrypto', 'seg', 'tablefunc',
|
|
102
127
|
'tcn', 'tsm_system_rows', 'tsm_system_time', 'unaccent', 'uuid_ossp',
|
|
103
128
|
];
|
|
129
|
+
const { PGlite } = await import('@electric-sql/pglite');
|
|
130
|
+
const { vector } = await import('@electric-sql/pglite-pgvector');
|
|
104
131
|
const extensions = { vector };
|
|
105
132
|
for (const name of CONTRIB) {
|
|
106
133
|
const extension = (await import(`@electric-sql/pglite/contrib/${name}`))[name];
|
|
@@ -134,7 +161,7 @@ process.stderr.on('error', () => {});
|
|
|
134
161
|
const initKeepAlive = setInterval(() => {}, 500);
|
|
135
162
|
const dbReady = PGlite.create({ dataDir, extensions });
|
|
136
163
|
dbReady.catch((error) => {
|
|
137
|
-
process.stderr.write(`pglite-host: backend failed to start: ${error
|
|
164
|
+
process.stderr.write(`pglite-host: backend failed to start: ${errorMessage(error)}\n`);
|
|
138
165
|
process.exit(1);
|
|
139
166
|
});
|
|
140
167
|
|
|
@@ -184,7 +211,7 @@ async function enter(token, session, data) {
|
|
|
184
211
|
if (host && !host.busy && !host.wanting && !host.midBatch && host.status === 'T'
|
|
185
212
|
&& session.status !== 'T' && session.status !== 'E' && guestMessageAllowed(session, data)) {
|
|
186
213
|
guestBatch = token;
|
|
187
|
-
guestStartedAt =
|
|
214
|
+
guestStartedAt = machineNow();
|
|
188
215
|
return 'guest';
|
|
189
216
|
}
|
|
190
217
|
}
|
|
@@ -451,7 +478,7 @@ async function flushClosed(db) {
|
|
|
451
478
|
try {
|
|
452
479
|
await db.execProtocolRaw(new Uint8Array(close));
|
|
453
480
|
await db.execProtocolRaw(new Uint8Array([0x53, 0, 0, 0, 4])); // Sync
|
|
454
|
-
} catch {
|
|
481
|
+
} catch (error) { process.stderr.write(`pglite-host: statement cleanup failed: ${errorMessage(error)}\n`); }
|
|
455
482
|
if (heldStatements.get(nameKey(backend))?.closed) heldStatements.delete(nameKey(backend));
|
|
456
483
|
}
|
|
457
484
|
}
|
|
@@ -500,7 +527,9 @@ const READY_IDLE = Buffer.from([0x5a, 0, 0, 0, 5, 0x49]);
|
|
|
500
527
|
|
|
501
528
|
/** A statement of the host's own around a guest; its failure (the holder's transaction already gone) is not fatal. */
|
|
502
529
|
async function quiet(db, sql) {
|
|
503
|
-
try { await db.query(sql); } catch {
|
|
530
|
+
try { await db.query(sql); } catch (error) {
|
|
531
|
+
if (error?.code !== '3B001' && error?.code !== '25P01') throw error;
|
|
532
|
+
}
|
|
504
533
|
}
|
|
505
534
|
|
|
506
535
|
/** `response` with its last ReadyForQuery reporting `status`. */
|
|
@@ -532,7 +561,7 @@ function idleInTransactionFatal() {
|
|
|
532
561
|
// the holder idle in its transaction past the limit while others wait: end it
|
|
533
562
|
const idleWatch = setInterval(() => {
|
|
534
563
|
// a guest holding its turn past GUEST_TURN_MS while others wait: its batch is refused, its savepoint undone
|
|
535
|
-
if (guestBatch !== null && waiters.length > 0 &&
|
|
564
|
+
if (guestBatch !== null && waiters.length > 0 && machineNow() - guestStartedAt > GUEST_TURN_MS) {
|
|
536
565
|
const guest = sessions.get(guestBatch);
|
|
537
566
|
const expired = guestBatch;
|
|
538
567
|
if (guest && guest.guestRefused && !guest.guestRunning) {
|
|
@@ -541,7 +570,7 @@ const idleWatch = setInterval(() => {
|
|
|
541
570
|
guest.guestExpired = true;
|
|
542
571
|
guest.guestErrorSent = true;
|
|
543
572
|
const host = sessions.get(holder);
|
|
544
|
-
if (host) host.idleSince +=
|
|
573
|
+
if (host) host.idleSince += machineNow() - guestStartedAt;
|
|
545
574
|
guestBatch = null;
|
|
546
575
|
wakeAll();
|
|
547
576
|
} else if (guest && guest.guestOpen && !guest.guestRunning) {
|
|
@@ -554,7 +583,7 @@ const idleWatch = setInterval(() => {
|
|
|
554
583
|
}).then(() => {
|
|
555
584
|
if (guestBatch !== expired) return;
|
|
556
585
|
const host = sessions.get(holder);
|
|
557
|
-
if (host) host.idleSince +=
|
|
586
|
+
if (host) host.idleSince += machineNow() - guestStartedAt;
|
|
558
587
|
guestBatch = null;
|
|
559
588
|
wakeAll();
|
|
560
589
|
});
|
|
@@ -565,7 +594,7 @@ const idleWatch = setInterval(() => {
|
|
|
565
594
|
// armed at ReadyForQuery inside a transaction, as Postgres arms it — never mid-pipeline
|
|
566
595
|
// a holder waiting for its turn (behind a guest) is sending, not idle; a guest's turn is not the holder's idleness
|
|
567
596
|
if (!session || session.busy || session.wanting || guestBatch !== null || session.terminated || (session.status !== 'T' && session.status !== 'E')) return;
|
|
568
|
-
if (
|
|
597
|
+
if (machineNow() - session.idleSince < IDLE_IN_TRANSACTION_MS) return;
|
|
569
598
|
session.terminated = true;
|
|
570
599
|
process.stderr.write('pglite-host: terminated a connection idle in its transaction while others waited\n');
|
|
571
600
|
// the socket's close handler rolls the transaction back and releases the lock
|
|
@@ -727,7 +756,7 @@ function withoutEarlyReady(message, response) {
|
|
|
727
756
|
const server = net.createServer(async (socket) => {
|
|
728
757
|
const token = {};
|
|
729
758
|
let holding = false;
|
|
730
|
-
const session = { socket, busy: false, idleSince:
|
|
759
|
+
const session = { socket, busy: false, idleSince: machineNow(), terminated: false, closed: false, status: null, midBatch: false,
|
|
731
760
|
wanting: false, guestOpen: false, guestRefused: false, guestUnnamed: false, guestExpired: false, guestErrorSent: false, guestRunning: false, statements: new Map(),
|
|
732
761
|
names: new Map() };
|
|
733
762
|
sessions.set(token, session);
|
|
@@ -747,7 +776,7 @@ const server = net.createServer(async (socket) => {
|
|
|
747
776
|
// next connection in — never leak a half-open transaction into a stranger
|
|
748
777
|
void dbReady
|
|
749
778
|
.then((db) => db.query('ROLLBACK'))
|
|
750
|
-
.catch(
|
|
779
|
+
.catch(error => { process.stderr.write(`pglite-host: connection cleanup failed: ${errorMessage(error)}\n`); })
|
|
751
780
|
.then(() => {
|
|
752
781
|
holding = false;
|
|
753
782
|
release(token);
|
|
@@ -765,6 +794,7 @@ const server = net.createServer(async (socket) => {
|
|
|
765
794
|
// after the closed connections' names are released (a reconnect's `s0` is its own again)
|
|
766
795
|
data = scopeNames(session, client, false);
|
|
767
796
|
const db = await dbReady;
|
|
797
|
+
readWorldClock();
|
|
768
798
|
// a guest whose turn ran out: told once, the rest of its batch dropped until its Sync
|
|
769
799
|
if (session.guestExpired) {
|
|
770
800
|
if (data[0] === 0x53) { session.guestExpired = false; session.guestErrorSent = false; session.status = 'I'; return READY_IDLE; }
|
|
@@ -785,7 +815,7 @@ const server = net.createServer(async (socket) => {
|
|
|
785
815
|
session.guestUnnamed = false;
|
|
786
816
|
// the holder's idle clock stops while a guest runs, and goes on from where it was
|
|
787
817
|
const host = sessions.get(holder);
|
|
788
|
-
if (host) host.idleSince +=
|
|
818
|
+
if (host) host.idleSince += machineNow() - guestStartedAt;
|
|
789
819
|
wakeAll();
|
|
790
820
|
};
|
|
791
821
|
// a batch refused mid-way: its remaining messages are dropped until its Sync, as Postgres skips after an error
|
|
@@ -811,7 +841,7 @@ const server = net.createServer(async (socket) => {
|
|
|
811
841
|
try {
|
|
812
842
|
raw = await db.execProtocolRaw(refuseStatements(data, own));
|
|
813
843
|
} catch (error) {
|
|
814
|
-
process.stderr.write(`pglite-host: backend aborted: ${error
|
|
844
|
+
process.stderr.write(`pglite-host: backend aborted: ${errorMessage(error)}\n`);
|
|
815
845
|
process.exit(1);
|
|
816
846
|
}
|
|
817
847
|
const response = withoutEarlyReady(data, raw);
|
|
@@ -847,11 +877,11 @@ const server = net.createServer(async (socket) => {
|
|
|
847
877
|
// SQL errors arrive as ErrorResponse bytes; a throw means the WASM
|
|
848
878
|
// backend itself aborted and every later call would hang. Exit so the
|
|
849
879
|
// World sees the service down (status fails) instead of a wedged port.
|
|
850
|
-
process.stderr.write(`pglite-host: backend aborted: ${error
|
|
880
|
+
process.stderr.write(`pglite-host: backend aborted: ${errorMessage(error)}\n`);
|
|
851
881
|
process.exit(1);
|
|
852
882
|
}
|
|
853
883
|
session.busy = false;
|
|
854
|
-
session.idleSince =
|
|
884
|
+
session.idleSince = machineNow();
|
|
855
885
|
const response = withoutEarlyReady(data, raw);
|
|
856
886
|
settleClose(session, closes, response);
|
|
857
887
|
const status = lastReadyStatus(response);
|
|
@@ -2,7 +2,8 @@ import type { WorldInstance } from './schema.js';
|
|
|
2
2
|
export type SeedWorldOptions = {
|
|
3
3
|
root?: string;
|
|
4
4
|
entry?: string;
|
|
5
|
-
cwd?: string;
|
|
5
|
+
cwd?: string; /** Reset writes a new live environment output; never an input file. */
|
|
6
|
+
envFile?: string;
|
|
6
7
|
};
|
|
7
8
|
export type SeedWorldOutcome = {
|
|
8
9
|
world: string;
|
|
@@ -14,7 +15,7 @@ export type SeedWorldOutcome = {
|
|
|
14
15
|
export declare function seedEntryFor(instance: WorldInstance, entry?: string): string;
|
|
15
16
|
/** Pull from the placeholder: every twin's window open around the seed entry (a twin with a root excepted). */
|
|
16
17
|
export declare function seedWorld(name: string, options?: SeedWorldOptions): Promise<SeedWorldOutcome>;
|
|
17
|
-
/** Back to the defaults: down with purge, up from the same config and env
|
|
18
|
+
/** Back to the defaults: down with purge, up from the same config and a fresh env output, seed. */
|
|
18
19
|
export declare function resetWorld(name: string, options?: SeedWorldOptions): Promise<SeedWorldOutcome & {
|
|
19
20
|
instance: WorldInstance;
|
|
20
21
|
}>;
|
package/dist/src/placeholder.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
// `diff` reads empty, nothing is pending. `reset` is a pull from the placeholder again — down
|
|
6
6
|
// with purge, up, seed — so a world returns to its defaults at any time; local work you want to
|
|
7
7
|
// keep is a fork, taken before.
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { claimEnvOutput } from "./env-output.js";
|
|
8
10
|
import { materializeRoots } from "./root.js";
|
|
9
11
|
import { loadWorldConfig } from "./configs.js";
|
|
10
12
|
import { WorldDoors, servedName } from "./served-world.js";
|
|
11
|
-
import { randomUUID } from 'node:crypto';
|
|
12
13
|
import { existsSync, readdirSync } from 'node:fs';
|
|
13
14
|
import { dirname, join, resolve } from 'node:path';
|
|
14
|
-
import { serveHttp, beginPlaceholderPull, endPlaceholderPull, listEvents, PLACEHOLDER_REMOTE, worldStateRoot } from '@volter/world-core/runtime';
|
|
15
|
+
import { serveHttp, beginPlaceholderPull, endPlaceholderPull, listEvents, PLACEHOLDER_REMOTE, stateDirName, worldStateRoot } from '@volter/world-core/runtime';
|
|
15
16
|
import { downWorld, runWithWorldEnv, saveWorldInstance, statusWorld, upWorld } from "./runtime.js";
|
|
16
17
|
/** The seed entry `init` emits beside the world config, unless the caller names one. */
|
|
17
18
|
export function seedEntryFor(instance, entry) {
|
|
@@ -89,18 +90,22 @@ export async function seedWorld(name, options = {}) {
|
|
|
89
90
|
observed[service] = placeholderCount(join(instance.dirs.data, service));
|
|
90
91
|
return { world: name, entry, exitCode, observed };
|
|
91
92
|
}
|
|
92
|
-
/** Back to the defaults: down with purge, up from the same config and env
|
|
93
|
+
/** Back to the defaults: down with purge, up from the same config and a fresh env output, seed. */
|
|
93
94
|
export async function resetWorld(name, options = {}) {
|
|
94
95
|
const root = resolve(options.root ?? process.cwd());
|
|
95
96
|
const before = statusWorld(name, root);
|
|
96
97
|
// A world whose twins ship no default data has no seed entry: back to the defaults is back to
|
|
97
98
|
// empty, and that is a reset too — not an error.
|
|
98
99
|
const entry = existsSync(resolve(options.entry ?? join(dirname(before.configPath), 'seed.ts'))) ? seedEntryFor(before, options.entry) : null;
|
|
100
|
+
// Purge removes the previous output's ownership receipt. Select and validate a fresh output before
|
|
101
|
+
// destroying state; never force an overwrite of a file that may hold credentials.
|
|
102
|
+
const envFile = resolve(options.envFile ?? join(dirname(before.envFile), `${name}.reset-${randomUUID()}.world.env`));
|
|
103
|
+
claimEnvOutput(envFile, join(root, stateDirName(), 'env-output-backups'));
|
|
99
104
|
const stopped = await downWorld(name, root, { purge: true });
|
|
100
105
|
if (!stopped.purged || stopped.externalErrors.length)
|
|
101
106
|
throw new Error(`World reset refused: teardown did not purge the old instance (${stopped.externalErrors.join('; ')})`);
|
|
102
107
|
// a world booted from a config PATH re-ups from that path; one booted by id, by id
|
|
103
|
-
const booted = await upWorld(existsSync(before.configPath) ? before.configPath : before.config, { name, root, mode: before.mode,
|
|
108
|
+
const booted = await upWorld(existsSync(before.configPath) ? before.configPath : before.config, { name, root, mode: before.mode, owner: before.lifecycle?.owner ?? before.resources?.owner, envFile });
|
|
104
109
|
// the origin belongs to the branch, not to its running state (git: a reset keeps the remote);
|
|
105
110
|
// the cursors do not — the mirror is gone, so the next fetch starts from the beginning
|
|
106
111
|
const instance = before.origin === undefined ? booted : { ...booted, origin: { url: before.origin.url, namespace: before.origin.namespace, ...(before.origin.clonedAt ? { clonedAt: before.origin.clonedAt } : {}) } };
|