@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) => {
|
|
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
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
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.
|
|
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.
|
|
78
|
+
"@volter/world-core": "3.0.90",
|
|
79
79
|
"@volter/world-runtime": "*",
|
|
80
80
|
"playwright": "^1.63.0"
|
|
81
81
|
},
|
package/src/walk/vendor-fetch.ts
CHANGED
|
@@ -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) => {
|
|
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
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
36
|
-
//
|
|
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
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const port =
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
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
|
}
|