@volter/twin-standard 1.0.80 → 1.0.82

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.
@@ -34,7 +34,7 @@ export async function vendorFetch(twinDir, vendor) {
34
34
  const bare = factory({ root, ...options });
35
35
  let bound;
36
36
  let host;
37
- const bind = () => (bound ??= startPgliteHost().then((started) => { hosts.push(started); host = started; return factory({ root, database: started.url, ...options }); }));
37
+ const bind = () => (bound ??= startPgliteHost().then((started) => { host = started; return factory({ root, database: started.url, ...options }); }));
38
38
  const served = (request) => bind().then((fetch) => { host.clock(worldNow()); return fetch(request); });
39
39
  // the World's database itself, for what an application sends it over its own wire (a SQL step: journey-kit.ts)
40
40
  const managedDatabaseUrl = () => bind().then(() => { host.clock(worldNow()); return host.url; });
@@ -44,13 +44,6 @@ export async function vendorFetch(twinDir, vendor) {
44
44
  return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
45
45
  };
46
46
  }
47
- const hosts = [];
48
- process.once('exit', () => { for (const host of hosts) {
49
- try {
50
- process.kill(host.pid, 'SIGTERM');
51
- }
52
- catch { /* ended */ }
53
- } });
54
47
  const jwtView = (field) => (body) => {
55
48
  const token = body?.[field];
56
49
  if (typeof token !== 'string')
@@ -1,12 +1,7 @@
1
- /** `clock(at)` sets the World clock the database reads (pglite-host.mjs: the World's time), frozen at the instant `at`. */
2
1
  export type PgliteHost = {
3
2
  url: string;
4
3
  port: number;
5
- pid: number;
6
4
  clock: (at: string) => void;
7
5
  stop: () => Promise<void>;
8
6
  };
9
- /** Start a PGlite host under node (PGlite's WASM init does not complete under bun) and wait until it serves. Its clock
10
- * is a file of the host's own, which the walk sets to the walked World's clock before each request (`clock`); its
11
- * port is the same for every walked World. Database randomness is not pinned. */
12
7
  export declare function startPgliteHost(): Promise<PgliteHost>;
@@ -1,13 +1,14 @@
1
- // The walker's managed database: the World's own containerless Postgres (world-runtime's pglite-host.mjs, the PGlite host a World
2
- // without a container runtime serves its postgres service with), started on a free loopback port in a temp dir and
3
- // stopped by its own PID, for a pack that declares `managedDatabase` (packages/twin-standard/src/walk/vendor-fetch.ts). Not on the serve path.
4
- import { spawn } from 'node:child_process';
5
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
- import { createServer, connect } from 'node:net';
1
+ // The walker's Postgres is a declared, task-owned World. The runtime supervises its lifetime and backing;
2
+ // the walker owns when to use and release it, never the backend's process or data directory.
3
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { createServer } from 'node:net';
7
5
  import { tmpdir } from 'node:os';
8
6
  import { createRequire } from 'node:module';
9
- import { join } from 'node:path';
10
- const HOST_SCRIPT = createRequire(import.meta.url).resolve('@volter/world-runtime/src/pglite-host.mjs');
7
+ import { dirname, extname, join } from 'node:path';
8
+ import { runWorld, setWorldClock, worldConfigDocument } from '@volter/world-runtime';
9
+ import { worldNow } from '@volter/world-core/runtime';
10
+ const runtimeEntry = createRequire(import.meta.url).resolve('@volter/world-runtime');
11
+ const infrastructureCli = join(dirname(runtimeEntry), `infra-cli${extname(runtimeEntry)}`);
11
12
  async function freePort() {
12
13
  return new Promise((done, fail) => {
13
14
  const server = createServer();
@@ -18,79 +19,81 @@ async function freePort() {
18
19
  });
19
20
  });
20
21
  }
21
- function answers(port) {
22
- return new Promise((done) => {
23
- const socket = connect(port, '127.0.0.1');
24
- socket.once('connect', () => { socket.destroy(); done(true); });
25
- socket.once('error', () => done(false));
26
- });
27
- }
28
- // The port a walked World declares its postgres service at, as a World's infrastructure file declares one: the same for
29
- // every World this process walks, so two fresh Worlds hand an application the same database URL (R9), each World's
30
- // host on it once the one before has stopped (a walk stops its World's host when it ends). A World walked while another
31
- // still holds it gets a port of its own.
22
+ // Sequential replays declare the same database endpoint. A concurrent walk declares its own port. Only a successful
23
+ // runtime teardown releases this slot; unknown cleanup never licenses reusing its port or deleting its state.
32
24
  let declaredPort;
33
25
  let declaredHeld = false;
34
- async function released(port) {
35
- for (const deadline = Date.now() + 5000; Date.now() < deadline; await new Promise((r) => setTimeout(r, 100)))
36
- if (!(await answers(port)))
37
- return true;
38
- return false;
39
- }
40
- /** Start a PGlite host under node (PGlite's WASM init does not complete under bun) and wait until it serves. Its clock
41
- * is a file of the host's own, which the walk sets to the walked World's clock before each request (`clock`); its
42
- * port is the same for every walked World. Database randomness is not pinned. */
43
26
  export async function startPgliteHost() {
44
- const dir = mkdtempSync(join(tmpdir(), 'walk-database-'));
45
- const clockFile = join(dir, 'world-clock');
46
- const declared = declaredPort !== undefined && !declaredHeld && (await released(declaredPort));
47
- const port = declared ? declaredPort : await freePort();
27
+ if (!process.env.VOLTER_WORLD)
28
+ throw new Error('A managed database walk requires its enclosing World');
29
+ const root = mkdtempSync(join(tmpdir(), 'walk-database-world-'));
30
+ const name = 'pack-walk-postgres';
31
+ const port = declaredPort !== undefined && !declaredHeld ? declaredPort : await freePort();
48
32
  declaredPort ??= port;
49
33
  const holdsDeclared = port === declaredPort;
50
34
  if (holdsDeclared)
51
35
  declaredHeld = true;
52
- // The host runs under a small guard that ends it (and removes its data) when this process's end closes the guard's
53
- // stdin: a test runner or a manifest run that exits without its teardown leaves no PGlite host behind.
54
- const guard = `
55
- import { spawn } from 'node:child_process'; import { rmSync } from 'node:fs';
56
- const [script, port, data, dir] = process.argv.slice(1);
57
- const host = spawn(process.execPath, [script, '--port', port, '--data', data], { stdio: ['ignore', 'ignore', 'inherit'] });
58
- const end = () => { try { host.kill('SIGTERM'); } catch {} setTimeout(() => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(0); }, 500); };
59
- process.stdin.on('end', end); process.stdin.on('close', end); process.stdin.resume();
60
- process.on('SIGTERM', end);
61
- host.on('exit', (code) => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(code ?? 0); });`;
62
- const child = spawn('node', ['--input-type=module', '-e', guard, HOST_SCRIPT, String(port), join(dir, 'pgdata'), dir], { stdio: ['pipe', 'ignore', 'pipe'], env: { ...process.env, TWIN_WORLD_CLOCK_FILE: clockFile } });
63
- let stderr = '';
64
- child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
65
- // the host never keeps the walk's process alive: its end closes the guard's stdin, which ends the host (a walk that
66
- // finished otherwise waited on these pipes forever, and grade-pack on its 600 s timeout)
67
- child.unref();
68
- child.stdin?.unref?.();
69
- child.stderr?.unref?.();
70
- const deadline = Date.now() + 60_000;
71
- while (!(await answers(port))) {
72
- if (child.exitCode !== null)
73
- throw new Error(`pglite-host exited ${child.exitCode}: ${stderr}`);
74
- if (Date.now() > deadline) {
75
- child.kill('SIGTERM');
76
- throw new Error(`pglite-host did not listen within 60s: ${stderr}`);
36
+ const fake = { user: 'world', database: 'world', password: 'walk-only-fake-password' };
37
+ const url = `postgresql://${fake.user}:${fake.password}@127.0.0.1:${port}/${fake.database}`;
38
+ const config = join(root, 'world.json');
39
+ const command = [process.execPath, infrastructureCli];
40
+ writeFileSync(join(root, 'world.infrastructure.yml'), JSON.stringify({ services: { postgres: {
41
+ image: 'postgres:18-alpine', ports: [`127.0.0.1:${port}:5432`],
42
+ environment: { POSTGRES_USER: fake.user, POSTGRES_DB: fake.database, POSTGRES_PASSWORD: fake.password },
43
+ } } }, null, 2) + '\n');
44
+ writeFileSync(config, JSON.stringify(worldConfigDocument({ id: name,
45
+ env: { DATABASE_URL: url, VOLTER_WORLD_INFRA_BACKING: 'pglite' }, network: { egress: [] },
46
+ services: [{ id: 'infrastructure', type: 'external', controlPlane: true, external: {
47
+ up: [...command, 'up'], status: [...command, 'status'], down: [...command, 'down'],
48
+ discover: [{ as: 'DATABASE_URL', jsonPath: 'connections.DATABASE_URL', protocol: 'postgres' }],
49
+ readyWhen: { command: command[0], args: [...command.slice(1), 'status'], timeoutMs: 120_000, intervalMs: 500 },
50
+ } }],
51
+ }), null, 2) + '\n');
52
+ setWorldClock(name, root, worldNow());
53
+ const readyFile = join(root, 'ready.json'), releaseFile = join(root, 'release');
54
+ // This is the registered consumer held by runWorld, not an infrastructure supervisor. It exposes only its own
55
+ // connection env, and exits when the caller's sequence is done. Caller loss belongs to the runtime's run guardian.
56
+ const hold = `
57
+ import { existsSync, renameSync, watch, writeFileSync } from 'node:fs';
58
+ import { dirname } from 'node:path';
59
+ const [ready, release] = process.argv.slice(1);
60
+ if (!process.env.VOLTER_WORLD || !process.env.DATABASE_URL) throw new Error('Missing held World connection');
61
+ const done = () => { if (existsSync(release)) process.exit(0); };
62
+ watch(dirname(release), done);
63
+ writeFileSync(ready + '.tmp', JSON.stringify({ url: process.env.DATABASE_URL }));
64
+ renameSync(ready + '.tmp', ready);
65
+ done();`;
66
+ let finished;
67
+ const task = runWorld(config, [process.execPath, '--input-type=module', '-e', hold, readyFile, releaseFile], {
68
+ root, cwd: root, owner: 'pack-walk-managed-database',
69
+ }).then(result => { finished = { result, failed: false }; return result; }, error => {
70
+ finished = { error, failed: true };
71
+ throw error;
72
+ });
73
+ // Startup and normal stop await this promise; keep an early rejection handled while waiting for readiness.
74
+ void task.catch(() => undefined);
75
+ while (!existsSync(readyFile)) {
76
+ if (finished) {
77
+ if (!finished.failed && holdsDeclared)
78
+ declaredHeld = false;
79
+ if (finished.failed)
80
+ throw finished.error;
81
+ throw new Error(`Managed database World ended before its consumer was ready (exit ${finished.result.exitCode}); evidence: ${root}`);
77
82
  }
78
- await new Promise((r) => setTimeout(r, 200));
83
+ await new Promise(resolve => setTimeout(resolve, 100));
79
84
  }
85
+ const ready = JSON.parse(readFileSync(readyFile, 'utf8'));
86
+ let stopping;
80
87
  return {
81
- url: `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`,
82
- port,
83
- pid: child.pid,
84
- clock: (at) => writeFileSync(clockFile, `${at}\n`),
85
- stop: async () => {
86
- if (child.exitCode === null && child.pid !== undefined) {
87
- const ended = new Promise((r) => child.once('exit', r));
88
- process.kill(child.pid, 'SIGTERM');
89
- await Promise.race([ended, new Promise((r) => setTimeout(r, 5000))]);
90
- }
91
- rmSync(dir, { recursive: true, force: true });
88
+ url: ready.url, port: Number(new URL(ready.url).port),
89
+ clock: at => setWorldClock(name, root, at),
90
+ stop: () => stopping ??= (async () => {
91
+ writeFileSync(releaseFile, 'released\n');
92
+ const result = await task;
93
+ if (result.exitCode !== 0)
94
+ throw new Error(`Managed database World consumer exited ${result.exitCode}; evidence: ${root}`);
92
95
  if (holdsDeclared)
93
96
  declaredHeld = false;
94
- },
97
+ })(),
95
98
  };
96
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-standard",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
4
4
  "description": "The Protocol 3 standard for Volter twin packs: the criteria a pack is graded by, the grader, the gate a pack repository and the twin catalog run on every change and submission, and the spec derivation the grade checks against. Pin a version: every place a pack is judged judges by that standard.",
5
5
  "keywords": [
6
6
  "twin",
@@ -75,7 +75,7 @@
75
75
  "yaml": "2.9.0"
76
76
  },
77
77
  "peerDependencies": {
78
- "@volter/world-core": "3.0.88",
78
+ "@volter/world-core": "3.0.90",
79
79
  "@volter/world-runtime": "*",
80
80
  "playwright": "^1.63.0"
81
81
  },
@@ -27,7 +27,7 @@ export async function vendorFetch(twinDir: string, vendor: string): Promise<Vend
27
27
  const bare = factory({ root, ...options });
28
28
  let bound: Promise<(request: Request) => Response | Promise<Response>> | undefined;
29
29
  let host: PgliteHost | undefined;
30
- const bind = () => (bound ??= startPgliteHost().then((started) => { hosts.push(started); host = started; return factory({ root, database: started.url, ...options }); }));
30
+ const bind = () => (bound ??= startPgliteHost().then((started) => { host = started; return factory({ root, database: started.url, ...options }); }));
31
31
  const served = (request: Request) => bind().then((fetch) => { host!.clock(worldNow()); return fetch(request); });
32
32
  // the World's database itself, for what an application sends it over its own wire (a SQL step: journey-kit.ts)
33
33
  const managedDatabaseUrl = () => bind().then(() => { host!.clock(worldNow()); return host!.url; });
@@ -37,8 +37,6 @@ export async function vendorFetch(twinDir: string, vendor: string): Promise<Vend
37
37
  };
38
38
  }
39
39
 
40
- const hosts: PgliteHost[] = [];
41
- process.once('exit', () => { for (const host of hosts) { try { process.kill(host.pid, 'SIGTERM'); } catch { /* ended */ } } });
42
40
 
43
41
  /** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
44
42
  * JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
@@ -1,14 +1,15 @@
1
- // The walker's managed database: the World's own containerless Postgres (world-runtime's pglite-host.mjs, the PGlite host a World
2
- // without a container runtime serves its postgres service with), started on a free loopback port in a temp dir and
3
- // stopped by its own PID, for a pack that declares `managedDatabase` (packages/twin-standard/src/walk/vendor-fetch.ts). Not on the serve path.
4
- import { spawn, type ChildProcess } from 'node:child_process';
5
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
- import { createServer, connect } from 'node:net';
1
+ // The walker's Postgres is a declared, task-owned World. The runtime supervises its lifetime and backing;
2
+ // the walker owns when to use and release it, never the backend's process or data directory.
3
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { createServer } from 'node:net';
7
5
  import { tmpdir } from 'node:os';
8
6
  import { createRequire } from 'node:module';
9
- import { join } from 'node:path';
7
+ import { dirname, extname, join } from 'node:path';
8
+ import { runWorld, setWorldClock, worldConfigDocument } from '@volter/world-runtime';
9
+ import { worldNow } from '@volter/world-core/runtime';
10
10
 
11
- const HOST_SCRIPT = createRequire(import.meta.url).resolve('@volter/world-runtime/src/pglite-host.mjs');
11
+ const runtimeEntry = createRequire(import.meta.url).resolve('@volter/world-runtime');
12
+ const infrastructureCli = join(dirname(runtimeEntry), `infra-cli${extname(runtimeEntry)}`);
12
13
 
13
14
  async function freePort(): Promise<number> {
14
15
  return new Promise((done, fail) => {
@@ -21,77 +22,77 @@ async function freePort(): Promise<number> {
21
22
  });
22
23
  }
23
24
 
24
- function answers(port: number): Promise<boolean> {
25
- return new Promise((done) => {
26
- const socket = connect(port, '127.0.0.1');
27
- socket.once('connect', () => { socket.destroy(); done(true); });
28
- socket.once('error', () => done(false));
29
- });
30
- }
31
-
32
- /** `clock(at)` sets the World clock the database reads (pglite-host.mjs: the World's time), frozen at the instant `at`. */
33
- export type PgliteHost = { url: string; port: number; pid: number; clock: (at: string) => void; stop: () => Promise<void> };
25
+ export type PgliteHost = { url: string; port: number; clock: (at: string) => void; stop: () => Promise<void> };
34
26
 
35
- // The port a walked World declares its postgres service at, as a World's infrastructure file declares one: the same for
36
- // every World this process walks, so two fresh Worlds hand an application the same database URL (R9), each World's
37
- // host on it once the one before has stopped (a walk stops its World's host when it ends). A World walked while another
38
- // still holds it gets a port of its own.
27
+ // Sequential replays declare the same database endpoint. A concurrent walk declares its own port. Only a successful
28
+ // runtime teardown releases this slot; unknown cleanup never licenses reusing its port or deleting its state.
39
29
  let declaredPort: number | undefined;
40
30
  let declaredHeld = false;
41
31
 
42
- async function released(port: number): Promise<boolean> {
43
- for (const deadline = Date.now() + 5000; Date.now() < deadline; await new Promise((r) => setTimeout(r, 100))) if (!(await answers(port))) return true;
44
- return false;
45
- }
46
-
47
- /** Start a PGlite host under node (PGlite's WASM init does not complete under bun) and wait until it serves. Its clock
48
- * is a file of the host's own, which the walk sets to the walked World's clock before each request (`clock`); its
49
- * port is the same for every walked World. Database randomness is not pinned. */
50
32
  export async function startPgliteHost(): Promise<PgliteHost> {
51
- const dir = mkdtempSync(join(tmpdir(), 'walk-database-'));
52
- const clockFile = join(dir, 'world-clock');
53
- const declared = declaredPort !== undefined && !declaredHeld && (await released(declaredPort));
54
- const port = declared ? declaredPort! : await freePort();
33
+ if (!process.env.VOLTER_WORLD) throw new Error('A managed database walk requires its enclosing World');
34
+ const root = mkdtempSync(join(tmpdir(), 'walk-database-world-'));
35
+ const name = 'pack-walk-postgres';
36
+ const port = declaredPort !== undefined && !declaredHeld ? declaredPort : await freePort();
55
37
  declaredPort ??= port;
56
38
  const holdsDeclared = port === declaredPort;
57
39
  if (holdsDeclared) declaredHeld = true;
58
- // The host runs under a small guard that ends it (and removes its data) when this process's end closes the guard's
59
- // stdin: a test runner or a manifest run that exits without its teardown leaves no PGlite host behind.
60
- const guard = `
61
- import { spawn } from 'node:child_process'; import { rmSync } from 'node:fs';
62
- const [script, port, data, dir] = process.argv.slice(1);
63
- const host = spawn(process.execPath, [script, '--port', port, '--data', data], { stdio: ['ignore', 'ignore', 'inherit'] });
64
- const end = () => { try { host.kill('SIGTERM'); } catch {} setTimeout(() => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(0); }, 500); };
65
- process.stdin.on('end', end); process.stdin.on('close', end); process.stdin.resume();
66
- process.on('SIGTERM', end);
67
- host.on('exit', (code) => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(code ?? 0); });`;
68
- const child: ChildProcess = spawn('node', ['--input-type=module', '-e', guard, HOST_SCRIPT, String(port), join(dir, 'pgdata'), dir], { stdio: ['pipe', 'ignore', 'pipe'], env: { ...process.env, TWIN_WORLD_CLOCK_FILE: clockFile } });
69
- let stderr = '';
70
- child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); });
71
- // the host never keeps the walk's process alive: its end closes the guard's stdin, which ends the host (a walk that
72
- // finished otherwise waited on these pipes forever, and grade-pack on its 600 s timeout)
73
- child.unref();
74
- (child.stdin as unknown as { unref?: () => void } | null)?.unref?.();
75
- (child.stderr as unknown as { unref?: () => void } | null)?.unref?.();
76
- const deadline = Date.now() + 60_000;
77
- while (!(await answers(port))) {
78
- if (child.exitCode !== null) throw new Error(`pglite-host exited ${child.exitCode}: ${stderr}`);
79
- if (Date.now() > deadline) { child.kill('SIGTERM'); throw new Error(`pglite-host did not listen within 60s: ${stderr}`); }
80
- await new Promise((r) => setTimeout(r, 200));
40
+ const fake = { user: 'world', database: 'world', password: 'walk-only-fake-password' };
41
+ const url = `postgresql://${fake.user}:${fake.password}@127.0.0.1:${port}/${fake.database}`;
42
+ const config = join(root, 'world.json');
43
+ const command = [process.execPath, infrastructureCli];
44
+ writeFileSync(join(root, 'world.infrastructure.yml'), JSON.stringify({ services: { postgres: {
45
+ image: 'postgres:18-alpine', ports: [`127.0.0.1:${port}:5432`],
46
+ environment: { POSTGRES_USER: fake.user, POSTGRES_DB: fake.database, POSTGRES_PASSWORD: fake.password },
47
+ } } }, null, 2) + '\n');
48
+ writeFileSync(config, JSON.stringify(worldConfigDocument({ id: name,
49
+ env: { DATABASE_URL: url, VOLTER_WORLD_INFRA_BACKING: 'pglite' }, network: { egress: [] },
50
+ services: [{ id: 'infrastructure', type: 'external', controlPlane: true, external: {
51
+ up: [...command, 'up'], status: [...command, 'status'], down: [...command, 'down'],
52
+ discover: [{ as: 'DATABASE_URL', jsonPath: 'connections.DATABASE_URL', protocol: 'postgres' }],
53
+ readyWhen: { command: command[0], args: [...command.slice(1), 'status'], timeoutMs: 120_000, intervalMs: 500 },
54
+ } }],
55
+ }), null, 2) + '\n');
56
+ setWorldClock(name, root, worldNow());
57
+ const readyFile = join(root, 'ready.json'), releaseFile = join(root, 'release');
58
+ // This is the registered consumer held by runWorld, not an infrastructure supervisor. It exposes only its own
59
+ // connection env, and exits when the caller's sequence is done. Caller loss belongs to the runtime's run guardian.
60
+ const hold = `
61
+ import { existsSync, renameSync, watch, writeFileSync } from 'node:fs';
62
+ import { dirname } from 'node:path';
63
+ const [ready, release] = process.argv.slice(1);
64
+ if (!process.env.VOLTER_WORLD || !process.env.DATABASE_URL) throw new Error('Missing held World connection');
65
+ const done = () => { if (existsSync(release)) process.exit(0); };
66
+ watch(dirname(release), done);
67
+ writeFileSync(ready + '.tmp', JSON.stringify({ url: process.env.DATABASE_URL }));
68
+ renameSync(ready + '.tmp', ready);
69
+ done();`;
70
+ let finished: { result?: Awaited<ReturnType<typeof runWorld>>; error?: unknown; failed: boolean } | undefined;
71
+ const task = runWorld(config, [process.execPath, '--input-type=module', '-e', hold, readyFile, releaseFile], {
72
+ root, cwd: root, owner: 'pack-walk-managed-database',
73
+ }).then(result => { finished = { result, failed: false }; return result; }, error => {
74
+ finished = { error, failed: true }; throw error;
75
+ });
76
+ // Startup and normal stop await this promise; keep an early rejection handled while waiting for readiness.
77
+ void task.catch(() => undefined);
78
+ while (!existsSync(readyFile)) {
79
+ if (finished) {
80
+ if (!finished.failed && holdsDeclared) declaredHeld = false;
81
+ if (finished.failed) throw finished.error;
82
+ throw new Error(`Managed database World ended before its consumer was ready (exit ${finished.result!.exitCode}); evidence: ${root}`);
83
+ }
84
+ await new Promise(resolve => setTimeout(resolve, 100));
81
85
  }
86
+ const ready = JSON.parse(readFileSync(readyFile, 'utf8')) as { url: string };
87
+ let stopping: Promise<void> | undefined;
82
88
  return {
83
- url: `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`,
84
- port,
85
- pid: child.pid!,
86
- clock: (at) => writeFileSync(clockFile, `${at}\n`),
87
- stop: async () => {
88
- if (child.exitCode === null && child.pid !== undefined) {
89
- const ended = new Promise((r) => child.once('exit', r));
90
- process.kill(child.pid, 'SIGTERM');
91
- await Promise.race([ended, new Promise((r) => setTimeout(r, 5000))]);
92
- }
93
- rmSync(dir, { recursive: true, force: true });
89
+ url: ready.url, port: Number(new URL(ready.url).port),
90
+ clock: at => setWorldClock(name, root, at),
91
+ stop: () => stopping ??= (async () => {
92
+ writeFileSync(releaseFile, 'released\n');
93
+ const result = await task;
94
+ if (result.exitCode !== 0) throw new Error(`Managed database World consumer exited ${result.exitCode}; evidence: ${root}`);
94
95
  if (holdsDeclared) declaredHeld = false;
95
- },
96
+ })(),
96
97
  };
97
98
  }