@volter/world-runtime 3.0.163 → 3.0.164
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/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +3 -2
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +445 -18
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +3 -3
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +3 -3
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +4 -3
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +416 -17
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +3 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
package/dist/src/covers.js
CHANGED
|
@@ -528,11 +528,11 @@ function usesOf(signals) {
|
|
|
528
528
|
}
|
|
529
529
|
/** Detect every vendor (and every unmapped external-service-shaped signal) an application repo
|
|
530
530
|
* betrays: mapped npm dependencies across the root + all workspace members, vendor-shaped env
|
|
531
|
-
* names in committed `.env*` files, the SMTP/raw-protocol signal, and literal vendor URLs passed
|
|
531
|
+
* names in committed `.env*` files or its declared stack, the SMTP/raw-protocol signal, and literal vendor URLs passed
|
|
532
532
|
* directly to fetch in production source. URL attribution reads the injector's VENDOR_HOSTS table
|
|
533
533
|
* rather than re-encoding host rules here. Insertion order follows the sorted dependency, env-name,
|
|
534
534
|
* and source lists, so callers that preserve it are deterministic. */
|
|
535
|
-
export function detectRepoVendors(repoPath, services = []) {
|
|
535
|
+
export function detectRepoVendors(repoPath, services = [], declaredEnvironmentNames = []) {
|
|
536
536
|
// Application aliases are names the selected backing actually injects, never global vendor adoption.
|
|
537
537
|
const backingEnvVendors = new Map();
|
|
538
538
|
for (const service of services) {
|
|
@@ -548,7 +548,11 @@ export function detectRepoVendors(repoPath, services = []) {
|
|
|
548
548
|
// A Volter package a pack names as its vendor's SDK is one (`@volter/identity`, the Volter identity
|
|
549
549
|
// service's client, is the volteridentity twin's), as any vendor's SDK is.
|
|
550
550
|
const dependencies = projectDependencies(repo).filter((dep) => Object.hasOwn(SDK_TWINS, dep) || (!dep.startsWith('@volter/') && !dep.startsWith('pypi:volter')));
|
|
551
|
-
const
|
|
551
|
+
for (const name of declaredEnvironmentNames) {
|
|
552
|
+
if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
553
|
+
throw new Error('Declared vendor environment name must be an identifier');
|
|
554
|
+
}
|
|
555
|
+
const envNames = [...new Set([...projectEnvNames(repo), ...declaredEnvironmentNames])].sort();
|
|
552
556
|
const npmRegistries = projectNpmRegistries(repo);
|
|
553
557
|
// vendor → detection sources
|
|
554
558
|
const detected = new Map();
|
|
@@ -734,7 +738,7 @@ export function coverWorld(world, repoPath, options = {}) {
|
|
|
734
738
|
}
|
|
735
739
|
const repo = resolve(repoPath);
|
|
736
740
|
const inventory = worldTwinInventory(world, root);
|
|
737
|
-
const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services);
|
|
741
|
+
const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services, options.declaredEnvironmentNames);
|
|
738
742
|
// Does ANY injector vendor key exist for this vendor? Decides whether an UNINTERCEPTABLE
|
|
739
743
|
// finding is acknowledgeable: a key that exists means the twin COULD be intercepted and the
|
|
740
744
|
// wiring is merely wrong (fix it, don't acknowledge it); no key at all means the pack is a
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Source-declared Go MySQL connection bindings; architecture, managed infrastructure. */
|
|
2
|
+
export type GoMysqlBinding = {
|
|
3
|
+
dsnFormat: 'go-mysql';
|
|
4
|
+
endpoint: 'explicit-port' | 'default-port';
|
|
5
|
+
dsnPrefix: '' | 'mysql://';
|
|
6
|
+
dsnProtocol: '' | 'tcp';
|
|
7
|
+
};
|
|
8
|
+
/** Only versioned, explicitly typed data selects a driver grammar. */
|
|
9
|
+
export declare function goMysqlBinding(entry: Record<string, unknown>, version: unknown, name: string, declaredPort: number): Partial<GoMysqlBinding>;
|
|
10
|
+
export declare function renderGoMysqlBinding(entry: GoMysqlBinding & {
|
|
11
|
+
name: string;
|
|
12
|
+
containerPort?: number;
|
|
13
|
+
query?: string;
|
|
14
|
+
}, connection: URL, port: number): string;
|
|
15
|
+
/** Match a declared DSN against its owned listener, never infer a grammar. */
|
|
16
|
+
export declare function goMysqlEndpoint(value: string, binding: GoMysqlBinding, declaredPort: number): {
|
|
17
|
+
host: string;
|
|
18
|
+
port: number;
|
|
19
|
+
} | undefined;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Only versioned, explicitly typed data selects a driver grammar. */
|
|
2
|
+
export function goMysqlBinding(entry, version, name, declaredPort) {
|
|
3
|
+
if (entry.dsnFormat === undefined) {
|
|
4
|
+
if (entry.endpoint !== undefined || entry.dsnPrefix !== undefined || entry.dsnProtocol !== undefined)
|
|
5
|
+
throw new Error(`volter init: ${name} has DSN fields without a declared format`);
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
if ((version !== 6 && version !== 7) || entry.dsnFormat !== 'go-mysql' || entry.scheme !== 'mysql' || entry.withPort !== undefined
|
|
9
|
+
|| (entry.endpoint !== 'explicit-port' && entry.endpoint !== 'default-port') || (entry.dsnPrefix !== '' && entry.dsnPrefix !== 'mysql://')
|
|
10
|
+
|| (entry.dsnProtocol !== '' && entry.dsnProtocol !== 'tcp') || !Number.isInteger(entry.containerPort) || Number(entry.containerPort) < 1 || Number(entry.containerPort) > 65535
|
|
11
|
+
|| (entry.endpoint === 'default-port' && entry.containerPort !== declaredPort)
|
|
12
|
+
|| (entry.dsnPrefix === 'mysql://' && entry.endpoint === 'explicit-port')
|
|
13
|
+
|| (entry.query !== undefined && (typeof entry.query !== 'string' || /[\u0000-\u001f\u007f]/u.test(entry.query)))) {
|
|
14
|
+
throw new Error(`volter init: ${name} needs a valid stack-needs version6/7 Go MySQL binding`);
|
|
15
|
+
}
|
|
16
|
+
return { dsnFormat: 'go-mysql', endpoint: entry.endpoint,
|
|
17
|
+
dsnPrefix: entry.dsnPrefix, dsnProtocol: entry.dsnProtocol };
|
|
18
|
+
}
|
|
19
|
+
export function renderGoMysqlBinding(entry, connection, port) {
|
|
20
|
+
if (connection.protocol !== 'mysql:' || entry.containerPort === undefined)
|
|
21
|
+
throw new Error(`volter init: ${entry.name} needs a supplied MySQL endpoint and declared vendor port`);
|
|
22
|
+
if (entry.endpoint === 'default-port' && port !== entry.containerPort)
|
|
23
|
+
throw new Error(`volter init: ${entry.name} needs a per-service host serving its declared default port; the shared-host high-port allocation cannot supply it`);
|
|
24
|
+
const user = decodeURIComponent(connection.username), password = decodeURIComponent(connection.password), database = decodeURIComponent(connection.pathname.slice(1));
|
|
25
|
+
if (!user || !password || !database || /[:\u0000-\u001f\u007f]/u.test(user) || /[\u0000-\u001f\u007f]/u.test(password))
|
|
26
|
+
throw new Error(`volter init: ${entry.name} cannot represent the supplied MySQL credentials in its declared driver grammar`);
|
|
27
|
+
if (entry.dsnPrefix === 'mysql://' && (entry.endpoint === 'explicit-port'
|
|
28
|
+
|| !/^[A-Za-z0-9\-._~!$&'()*+,;=:%@]*$/u.test(`${user}:${password}`) || /%(?![A-Fa-f0-9]{2})/u.test(`${user}:${password}`))) {
|
|
29
|
+
throw new Error(`volter init: ${entry.name} cannot preserve its raw credentials/port through the declared mysql:// URL preparse`);
|
|
30
|
+
}
|
|
31
|
+
const address = `${connection.hostname}${entry.endpoint === 'explicit-port' ? `:${port}` : ''}`;
|
|
32
|
+
return `${entry.dsnPrefix}${user}:${password}@${entry.dsnProtocol}(${address})/${encodeURIComponent(database)}${entry.query !== undefined ? `?${entry.query}` : ''}`;
|
|
33
|
+
}
|
|
34
|
+
/** Match a declared DSN against its owned listener, never infer a grammar. */
|
|
35
|
+
export function goMysqlEndpoint(value, binding, declaredPort) {
|
|
36
|
+
if (/[\u0000-\u001f\u007f]/u.test(value) || !value.startsWith(binding.dsnPrefix))
|
|
37
|
+
return undefined;
|
|
38
|
+
const body = value.slice(binding.dsnPrefix.length), slash = body.lastIndexOf('/');
|
|
39
|
+
if (slash < 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
const authority = body.slice(0, slash), at = authority.lastIndexOf('@');
|
|
42
|
+
const transport = authority.slice(at + 1), begin = `${binding.dsnProtocol}(`;
|
|
43
|
+
if (!transport.startsWith(begin) || !transport.endsWith(')'))
|
|
44
|
+
return undefined;
|
|
45
|
+
const endpoint = /^(?:\[([^\]\s]+)\]|([^:\s/?#()]+))(?::([0-9]{1,5}))?$/u.exec(transport.slice(begin.length, -1));
|
|
46
|
+
if (!endpoint || (binding.endpoint === 'default-port' && endpoint[3] !== undefined)
|
|
47
|
+
|| (binding.endpoint === 'explicit-port' && endpoint[3] === undefined))
|
|
48
|
+
return undefined;
|
|
49
|
+
const port = endpoint[3] === undefined ? declaredPort : Number(endpoint[3]);
|
|
50
|
+
return port >= 1 && port <= 65535 ? { host: endpoint[1] ?? endpoint[2], port } : undefined;
|
|
51
|
+
}
|
|
@@ -5,5 +5,5 @@ export interface GuestProcessLaunch { path: string; record: Omit<GuestProcessRec
|
|
|
5
5
|
export function guestProcessDirectory(instanceDir: string): string;
|
|
6
6
|
export function beginGuestProcess(directory: string, instance: string, guest: string, kind: GuestProcessRecord['kind']): GuestProcessLaunch;
|
|
7
7
|
export function retainGuestProcess(receipt: GuestProcessLaunch, identity: SpawnIdentity): void;
|
|
8
|
-
export function readGuestProcesses(instanceDir: string, expectedInstance?: string): GuestProcessReceipt[];
|
|
8
|
+
export function readGuestProcesses(instanceDir: string, expectedInstance?: string, onError?: (error: unknown) => void): GuestProcessReceipt[];
|
|
9
9
|
export function releaseGuestProcesses(receipts: Array<{ path: string }>): void;
|
|
@@ -16,7 +16,9 @@ export function retainGuestProcess(receipt, identity) {
|
|
|
16
16
|
receipt.record = { ...receipt.record, launching: false, ...identity };
|
|
17
17
|
publishProcessReceipt(receipt.path, JSON.stringify(receipt.record));
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
// Teardown can retain each refusal while joining the other independently valid receipts.
|
|
20
|
+
// Other readers remain strict unless they explicitly collect these errors.
|
|
21
|
+
export function readGuestProcesses(instanceDir, expectedInstance, onError) {
|
|
20
22
|
const directory = guestProcessDirectory(instanceDir);
|
|
21
23
|
if (!existsSync(directory)) return [];
|
|
22
24
|
if (lstatSync(directory).isSymbolicLink()) throw new Error('Guest receipt directory is a symbolic link; ownership retained');
|
|
@@ -26,19 +28,25 @@ export function readGuestProcesses(instanceDir, expectedInstance) {
|
|
|
26
28
|
try { expectedInstance = JSON.parse(readFileSync(join(instanceDir, 'instance.json'), 'utf8')).instance; }
|
|
27
29
|
catch { throw new Error('Guest instance identity unreadable; ownership retained'); }
|
|
28
30
|
}
|
|
29
|
-
return names.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
return names.flatMap(name => {
|
|
32
|
+
try {
|
|
33
|
+
const path = join(directory, name);
|
|
34
|
+
if (!/^[0-9a-f-]+\.json$/.test(name) || !lstatSync(path).isFile() || lstatSync(path).isSymbolicLink()) throw new Error('Unresolved guest receipt publication; ownership retained');
|
|
35
|
+
const receipt = readProcessReceipt(path, text => {
|
|
36
|
+
const r = JSON.parse(text);
|
|
37
|
+
if (r.instance !== expectedInstance || typeof r.guest !== 'string' || !['agent', 'command', 'pty'].includes(r.kind)
|
|
38
|
+
|| r.launching || !Number.isInteger(r.pid) || r.pid < 1 || (r.start !== undefined && (typeof r.start !== 'string' || !r.start))) {
|
|
39
|
+
throw new Error('Guest launch or spawn identity unresolved');
|
|
40
|
+
}
|
|
41
|
+
return r;
|
|
42
|
+
});
|
|
43
|
+
if (receipt.error || !receipt.value) throw receipt.error ?? new Error('Guest receipt disappeared during inspection; ownership retained');
|
|
44
|
+
return [{ path, record: receipt.value }];
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (!onError) throw error;
|
|
47
|
+
onError(error);
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
// The caller must have confirmed each recorded group absent before releasing these receipts.
|
package/dist/src/host-cli.js
CHANGED
|
@@ -14,6 +14,9 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
|
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { startColocatedHost } from "./host.js";
|
|
16
16
|
import { publishHostReady } from "./host-ready.js";
|
|
17
|
+
// When this entry runs: the host's own modules are loaded; each twin's are loaded by its factory (S2 R38).
|
|
18
|
+
const began = Date.now();
|
|
19
|
+
const startedAt = {};
|
|
17
20
|
const argv = process.argv.slice(2);
|
|
18
21
|
const opt = (name, fb = '') => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] ? argv[i + 1] : fb; };
|
|
19
22
|
const isolation = opt('--isolation', 'shared');
|
|
@@ -83,6 +86,8 @@ function recordGaveUp(twin, exits, detail) {
|
|
|
83
86
|
const host = await startColocatedHost(specs, {
|
|
84
87
|
isolation,
|
|
85
88
|
onEvent: (e) => {
|
|
89
|
+
if (e.kind === 'started')
|
|
90
|
+
startedAt[e.twin] = Date.now();
|
|
86
91
|
process.stdout.write(`event twin=${e.twin} kind=${e.kind}${e.detail ? ` detail=${e.detail}` : ''}\n`);
|
|
87
92
|
if (e.kind === 'gaveup')
|
|
88
93
|
recordGaveUp(e.twin, e.exits ?? 0, e.detail ?? '');
|
|
@@ -94,13 +99,15 @@ process.on('SIGINT', shutdown);
|
|
|
94
99
|
const readyFile = opt('--ready-file');
|
|
95
100
|
if (readyFile) {
|
|
96
101
|
try {
|
|
97
|
-
publishHostReady(readyFile, process.env.VOLTER_WORLD_BOOT_ID ?? '', host.ports);
|
|
102
|
+
publishHostReady(readyFile, process.env.VOLTER_WORLD_BOOT_ID ?? '', host.ports, { began, started: startedAt }, host.streams);
|
|
98
103
|
}
|
|
99
104
|
catch (error) {
|
|
100
105
|
await host.stop();
|
|
101
106
|
throw error;
|
|
102
107
|
}
|
|
103
108
|
}
|
|
109
|
+
// the twins' cold first requests, taken in the host's idle time after its receipt rather than by the application's first calls
|
|
110
|
+
void host.warm();
|
|
104
111
|
for (const [id, port] of Object.entries(host.ports))
|
|
105
112
|
process.stdout.write(`ready id=${id} port=${port}\n`);
|
|
106
113
|
process.stdout.write(`host ready (${specs.length} twins, isolation=${isolation})\n`);
|
package/dist/src/host-ready.d.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
/** When the host's own start reached each step, in epoch milliseconds: its entry running (its static modules loaded),
|
|
2
|
+
* and each twin's factory returning. */
|
|
3
|
+
export type HostStartTimes = {
|
|
4
|
+
began: number;
|
|
5
|
+
started: Record<string, number>;
|
|
6
|
+
};
|
|
7
|
+
/** What a receipt says of the host's start, for the runtime's progress line, and which of its twins' main ports are byte
|
|
8
|
+
* streams (host.ts servesByteStream), for their admission; absent from a host that predates it. */
|
|
9
|
+
export type HostReadyTimes = {
|
|
10
|
+
at?: number;
|
|
11
|
+
streams?: string[];
|
|
12
|
+
} & Partial<HostStartTimes>;
|
|
1
13
|
/** Only the host, after all starts have returned and ports were checked, publishes this receipt. */
|
|
2
|
-
export declare function publishHostReady(path: string, boot: string, ports: Record<string, number
|
|
14
|
+
export declare function publishHostReady(path: string, boot: string, ports: Record<string, number>, times?: HostStartTimes, streams?: readonly string[]): void;
|
|
3
15
|
/**
|
|
4
16
|
* Receipt admission is cancellation-aware; stale or malformed state is never
|
|
5
17
|
* readiness. `timeoutMs` bounds a host that stops making progress, not a slow
|
|
6
18
|
* one: each change in `progress` (the host log's size, which grows with every
|
|
7
19
|
* twin it starts) begins the bound again, so a host still starting its twins
|
|
8
20
|
* on a busy machine is waited for, and one silent for `timeoutMs` is not.
|
|
21
|
+
* Resolves with what the receipt says of the host's start: when it was published, its steps before, and which declared
|
|
22
|
+
* twins' main ports are byte streams.
|
|
9
23
|
*/
|
|
10
|
-
export declare function waitForHostReady(path: string, boot: string, ports: ReadonlyMap<string, number>, timeoutMs: number, abort: AbortSignal, progress?: () => number): Promise<
|
|
24
|
+
export declare function waitForHostReady(path: string, boot: string, ports: ReadonlyMap<string, number>, timeoutMs: number, abort: AbortSignal, progress?: () => number): Promise<HostReadyTimes>;
|
package/dist/src/host-ready.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, watch, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
/** Only the host, after all starts have returned and ports were checked, publishes this receipt. */
|
|
4
|
-
export function publishHostReady(path, boot, ports) {
|
|
4
|
+
export function publishHostReady(path, boot, ports, times, streams) {
|
|
5
5
|
if (!boot)
|
|
6
6
|
throw new Error('A co-located host readiness receipt requires its World boot identity.');
|
|
7
7
|
mkdirSync(dirname(path), { recursive: true });
|
|
8
8
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
9
|
-
|
|
9
|
+
// `at`, when it was published, lets the runtime say how long its read came after, and `times` where the host's own
|
|
10
|
+
// start went before it (S2 R28, R38).
|
|
11
|
+
writeFileSync(temporary, `${JSON.stringify({ version: 1, boot, ports, at: Date.now(), ...times, ...(streams?.length ? { streams } : {}) })}\n`, { mode: 0o600 });
|
|
10
12
|
renameSync(temporary, path);
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
@@ -15,46 +17,71 @@ export function publishHostReady(path, boot, ports) {
|
|
|
15
17
|
* one: each change in `progress` (the host log's size, which grows with every
|
|
16
18
|
* twin it starts) begins the bound again, so a host still starting its twins
|
|
17
19
|
* on a busy machine is waited for, and one silent for `timeoutMs` is not.
|
|
20
|
+
* Resolves with what the receipt says of the host's start: when it was published, its steps before, and which declared
|
|
21
|
+
* twins' main ports are byte streams.
|
|
18
22
|
*/
|
|
19
23
|
export async function waitForHostReady(path, boot, ports, timeoutMs, abort, progress = () => 0) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
// A change in the receipt's directory (the host's rename is one) wakes the wait at once, where the 25 ms timer
|
|
25
|
+
// alone left a published receipt unread for up to a tick (S2 R28). The timer stays: a filesystem that gives no
|
|
26
|
+
// change events is read as before. Any other write in the directory wakes it too, and costs one read.
|
|
27
|
+
let wake;
|
|
28
|
+
let watcher;
|
|
29
|
+
try {
|
|
30
|
+
watcher = watch(dirname(path), { persistent: false }, () => wake?.());
|
|
31
|
+
watcher.on('error', () => { watcher?.close(); watcher = undefined; });
|
|
32
|
+
}
|
|
33
|
+
catch { /* no change events here: the timer alone */ }
|
|
34
|
+
try {
|
|
35
|
+
let started = Date.now();
|
|
36
|
+
let seen = progress();
|
|
37
|
+
for (;;) {
|
|
38
|
+
abort.throwIfAborted();
|
|
39
|
+
let bytes;
|
|
40
|
+
try {
|
|
41
|
+
bytes = readFileSync(path);
|
|
38
42
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code !== 'ENOENT')
|
|
45
|
+
throw error;
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (bytes) {
|
|
48
|
+
if (bytes.byteLength > 256 * 1024)
|
|
49
|
+
throw new Error('The co-located host readiness receipt exceeds its bounds.');
|
|
50
|
+
const value = JSON.parse(bytes.toString('utf8'));
|
|
51
|
+
if (!value || value.version !== 1 || value.boot !== boot || !value.ports || typeof value.ports !== 'object' || Array.isArray(value.ports)) {
|
|
52
|
+
throw new Error('The co-located host readiness receipt does not belong to this boot.');
|
|
53
|
+
}
|
|
54
|
+
const answered = value.ports;
|
|
55
|
+
if (Object.keys(answered).length !== ports.size || [...ports].some(([id, port]) => !Object.hasOwn(answered, id) || answered[id] !== port)) {
|
|
56
|
+
throw new Error('The co-located host readiness receipt does not cover its declared ports.');
|
|
57
|
+
}
|
|
58
|
+
const when = (time) => typeof time === 'number' && Number.isFinite(time) ? time : undefined;
|
|
59
|
+
// only the declared twins' times: an id the runtime did not declare never reaches its progress line
|
|
60
|
+
const twinStarts = value.started && typeof value.started === 'object' && !Array.isArray(value.started)
|
|
61
|
+
? Object.fromEntries(Object.entries(value.started).flatMap(([id, time]) => !ports.has(id) || when(time) === undefined ? [] : [[id, time]]))
|
|
62
|
+
: undefined;
|
|
63
|
+
const streams = Array.isArray(value.streams) ? value.streams.filter((id) => typeof id === 'string' && ports.has(id)) : [];
|
|
64
|
+
return { at: when(value.at), began: when(value.began), ...(twinStarts ? { started: twinStarts } : {}), ...(streams.length ? { streams } : {}) };
|
|
65
|
+
}
|
|
66
|
+
const now = progress();
|
|
67
|
+
if (now !== seen) {
|
|
68
|
+
seen = now;
|
|
69
|
+
started = Date.now();
|
|
70
|
+
}
|
|
71
|
+
if (Date.now() - started >= timeoutMs)
|
|
72
|
+
throw new Error(`The co-located host made no progress for ${timeoutMs} ms before completing every declared start.`);
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
const settle = () => { clearTimeout(timer); wake = undefined; abort.removeEventListener('abort', cancel); };
|
|
75
|
+
const cancel = () => { settle(); reject(abort.reason); };
|
|
76
|
+
const timer = setTimeout(() => { settle(); resolve(); }, Math.min(25, Math.max(1, timeoutMs - (Date.now() - started))));
|
|
77
|
+
wake = () => { settle(); resolve(); };
|
|
78
|
+
abort.addEventListener('abort', cancel, { once: true });
|
|
79
|
+
if (abort.aborted)
|
|
80
|
+
cancel();
|
|
81
|
+
});
|
|
49
82
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const cancel = () => { clearTimeout(timer); abort.removeEventListener('abort', cancel); reject(abort.reason); };
|
|
54
|
-
const timer = setTimeout(() => { abort.removeEventListener('abort', cancel); resolve(); }, Math.min(25, Math.max(1, timeoutMs - (Date.now() - started))));
|
|
55
|
-
abort.addEventListener('abort', cancel, { once: true });
|
|
56
|
-
if (abort.aborted)
|
|
57
|
-
cancel();
|
|
58
|
-
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
watcher?.close();
|
|
59
86
|
}
|
|
60
87
|
}
|
package/dist/src/host-worker.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getPack, registerPack } from '@volter/world-core/runtime';
|
|
|
4
4
|
// host does NOT restart it: it gives up on the worker (fires a `gaveup` event → doctor
|
|
5
5
|
// red) without disturbing sibling twins. Posts {type:'ready'} once listening.
|
|
6
6
|
import { parentPort, workerData } from 'node:worker_threads';
|
|
7
|
+
import { servesByteStream, servesDiscovery, warmInProcess } from "./host.js";
|
|
7
8
|
import { importModule } from "./import-module.js";
|
|
8
9
|
const spec = workerData;
|
|
9
10
|
const mod = await importModule(spec.module);
|
|
@@ -17,5 +18,10 @@ if (typeof factory !== 'function') {
|
|
|
17
18
|
}
|
|
18
19
|
// Awaited: an async factory (fly) must post `ready` only once its listener is bound.
|
|
19
20
|
const server = await factory({ ...(spec.sideOptions ?? {}), port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
20
|
-
parentPort?.postMessage({ type: 'ready', port: server.port });
|
|
21
|
+
parentPort?.postMessage({ type: 'ready', port: server.port, byteStream: servesByteStream(mod, descriptor), discovery: servesDiscovery(mod, descriptor) });
|
|
22
|
+
// the host's warm-up, taken here where this twin's server and its handler are (host.ts warmInProcess)
|
|
23
|
+
parentPort?.on('message', (msg) => {
|
|
24
|
+
if (msg?.type === 'warm')
|
|
25
|
+
void warmInProcess(server.port).then(() => parentPort?.postMessage({ type: 'warmed' }));
|
|
26
|
+
});
|
|
21
27
|
// The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
|
package/dist/src/host.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TwinPack } from '@volter/world-core/runtime';
|
|
1
2
|
/** One twin to mount in the host. `module`/`export` name a `({port,root,readOnly}) => {port,stop}`
|
|
2
3
|
* factory (every pack ships one, e.g. `createStripeTwinServer`). `module` is anything import()
|
|
3
4
|
* resolves: a package name (`@volter/twin-stripe`) or an absolute file path (tests/fixtures). */
|
|
@@ -22,8 +23,33 @@ export type ColocatedHost = {
|
|
|
22
23
|
isolation: HostIsolation;
|
|
23
24
|
/** Resolved listening port per twin id (equals the requested port). */
|
|
24
25
|
ports: Record<string, number>;
|
|
26
|
+
/** The twins whose main port is a byte stream (servesByteStream). */
|
|
27
|
+
streams: string[];
|
|
28
|
+
/** The twins whose main port answers the discovery door (GET /twin: the manifest declares `discovery`), warmed by it. */
|
|
29
|
+
discoverable: string[];
|
|
30
|
+
/** Warm every discoverable twin (warmInProcess, in its own thread under worker isolation); never needed for readiness. */
|
|
31
|
+
warm: () => Promise<void>;
|
|
25
32
|
stop: () => Promise<void>;
|
|
26
33
|
};
|
|
34
|
+
/** Whether a twin's main port is a byte stream: a raw-tcp pack exporting exactly one `create<Name>TwinStream` is taken
|
|
35
|
+
* to serve that stream there (world-core serveStream: an SMTP relay greets, a Redis server answers RESP). Such a port
|
|
36
|
+
* cannot answer an HTTP request, so the runtime admits it by its TCP answer (runtime.ts, startColocatedServices). Read
|
|
37
|
+
* off the module's own exports, as its serve factory is (twin-standard pack-facts.ts serveExport). A pack whose main
|
|
38
|
+
* port serves HTTP or gRPC must not declare raw-tcp with a stream export (create-pack refuses it). */
|
|
39
|
+
export declare function servesByteStream(mod: Record<string, unknown>, descriptor: TwinPack): boolean;
|
|
40
|
+
/** Whether a twin's main port answers the discovery door: its manifest declares `discovery`, and its port is HTTP. */
|
|
41
|
+
export declare function servesDiscovery(mod: Record<string, unknown>, descriptor: TwinPack): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Each twin's first request pays its pipeline's cold start (decoding, routing, the pack's front, first-run code), which
|
|
44
|
+
* otherwise falls on the application's first call to it. Its size for an HTTP twin is unmeasured (a byte-stream twin's
|
|
45
|
+
* first SMTP greeting took 57 ms against 1.8 ms later, e946aa4c run 3 on the mini, but that twin is skipped here); the
|
|
46
|
+
* R38 timing lap sizes it. The host takes it in its own idle time once its
|
|
47
|
+
* receipt is published: one GET of the twin's discovery door, handed in this process to the handler serving its port
|
|
48
|
+
* (world-core servedHandlerOf), never on the wire and never in its request journal. Bounded, its answer and any failure
|
|
49
|
+
* ignored; a request the application sends first is served as it would have been, and simply is that twin's warm-up.
|
|
50
|
+
* The discovery door reads no stored state, so a twin's first state read stays cold.
|
|
51
|
+
*/
|
|
52
|
+
export declare function warmInProcess(port: number): Promise<void>;
|
|
27
53
|
/**
|
|
28
54
|
* Start a co-located host. Resolves once every twin is listening. The returned `stop()`
|
|
29
55
|
* tears the whole host down (all servers / all workers).
|
package/dist/src/host.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from "./error-message.js";
|
|
2
|
-
import { getPack, registerPack } from '@volter/world-core/runtime';
|
|
2
|
+
import { getPack, registerPack, servedHandlerOf } from '@volter/world-core/runtime';
|
|
3
3
|
// Co-located twin host — run many twins in ONE process instead of one OS process
|
|
4
4
|
// per twin. The twin core is a pure, transport-agnostic handler (see a pack's
|
|
5
5
|
// `handle<Vendor>TwinRequest`), so nothing requires a process per twin; this host
|
|
@@ -26,6 +26,59 @@ import { siblingScript } from "./sibling.js";
|
|
|
26
26
|
import { importModule } from "./import-module.js";
|
|
27
27
|
import { pathToFileURL } from 'node:url';
|
|
28
28
|
import { parallelStartup } from "./startup.js";
|
|
29
|
+
/** Whether a twin's main port is a byte stream: a raw-tcp pack exporting exactly one `create<Name>TwinStream` is taken
|
|
30
|
+
* to serve that stream there (world-core serveStream: an SMTP relay greets, a Redis server answers RESP). Such a port
|
|
31
|
+
* cannot answer an HTTP request, so the runtime admits it by its TCP answer (runtime.ts, startColocatedServices). Read
|
|
32
|
+
* off the module's own exports, as its serve factory is (twin-standard pack-facts.ts serveExport). A pack whose main
|
|
33
|
+
* port serves HTTP or gRPC must not declare raw-tcp with a stream export (create-pack refuses it). */
|
|
34
|
+
export function servesByteStream(mod, descriptor) {
|
|
35
|
+
return descriptor.transport === 'raw-tcp'
|
|
36
|
+
&& Object.keys(mod).filter((key) => /^create\w*TwinStream$/.test(key) && typeof mod[key] === 'function').length === 1;
|
|
37
|
+
}
|
|
38
|
+
/** Whether a twin's main port answers the discovery door: its manifest declares `discovery`, and its port is HTTP. */
|
|
39
|
+
export function servesDiscovery(mod, descriptor) {
|
|
40
|
+
const manifest = mod.manifest;
|
|
41
|
+
return manifest?.discovery !== undefined && !servesByteStream(mod, descriptor);
|
|
42
|
+
}
|
|
43
|
+
/** The most twins warmed at once (S2 R41). */
|
|
44
|
+
const WARM_CONCURRENCY = 2;
|
|
45
|
+
/** A warm-up's bound: one that has not finished by then is left, its twin as it is. */
|
|
46
|
+
const WARM_TIMEOUT_MS = 2000;
|
|
47
|
+
/**
|
|
48
|
+
* Each twin's first request pays its pipeline's cold start (decoding, routing, the pack's front, first-run code), which
|
|
49
|
+
* otherwise falls on the application's first call to it. Its size for an HTTP twin is unmeasured (a byte-stream twin's
|
|
50
|
+
* first SMTP greeting took 57 ms against 1.8 ms later, e946aa4c run 3 on the mini, but that twin is skipped here); the
|
|
51
|
+
* R38 timing lap sizes it. The host takes it in its own idle time once its
|
|
52
|
+
* receipt is published: one GET of the twin's discovery door, handed in this process to the handler serving its port
|
|
53
|
+
* (world-core servedHandlerOf), never on the wire and never in its request journal. Bounded, its answer and any failure
|
|
54
|
+
* ignored; a request the application sends first is served as it would have been, and simply is that twin's warm-up.
|
|
55
|
+
* The discovery door reads no stored state, so a twin's first state read stays cold.
|
|
56
|
+
*/
|
|
57
|
+
export async function warmInProcess(port) {
|
|
58
|
+
const handler = servedHandlerOf(port);
|
|
59
|
+
if (!handler)
|
|
60
|
+
return;
|
|
61
|
+
let timer;
|
|
62
|
+
try {
|
|
63
|
+
await Promise.race([
|
|
64
|
+
(async () => { const response = await handler(new Request(`http://127.0.0.1:${port}/twin`)); await response?.arrayBuffer(); })(),
|
|
65
|
+
new Promise((resolve) => { timer = setTimeout(resolve, WARM_TIMEOUT_MS); timer.unref?.(); }),
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
catch { /* a warm-up that fails leaves its twin as it was */ }
|
|
69
|
+
finally {
|
|
70
|
+
if (timer !== undefined)
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Each of `ids` warmed by `warm`, at most WARM_CONCURRENCY at once. */
|
|
75
|
+
async function warmEach(ids, warm) {
|
|
76
|
+
const queue = [...ids];
|
|
77
|
+
await Promise.all(Array.from({ length: Math.min(WARM_CONCURRENCY, queue.length) }, async () => {
|
|
78
|
+
for (let id = queue.shift(); id !== undefined; id = queue.shift())
|
|
79
|
+
await warm(id);
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
29
82
|
async function loadFactory(spec) {
|
|
30
83
|
const mod = await importModule(spec.module);
|
|
31
84
|
const descriptor = mod.pack ?? getPack(spec.id);
|
|
@@ -36,7 +89,7 @@ async function loadFactory(spec) {
|
|
|
36
89
|
if (typeof factory !== 'function') {
|
|
37
90
|
throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
|
|
38
91
|
}
|
|
39
|
-
return factory;
|
|
92
|
+
return { factory: factory, byteStream: servesByteStream(mod, descriptor), discovery: servesDiscovery(mod, descriptor) };
|
|
40
93
|
}
|
|
41
94
|
const workerEntry = () => pathToFileURL(siblingScript(import.meta.url, 'host-worker'));
|
|
42
95
|
/** Per-twin stop budget inside host.stop(). Must stay comfortably under downWorld's 5s
|
|
@@ -57,6 +110,8 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
57
110
|
const ports = {};
|
|
58
111
|
for (const spec of specs)
|
|
59
112
|
ports[spec.id] = spec.port;
|
|
113
|
+
const streams = [];
|
|
114
|
+
const discoverable = [];
|
|
60
115
|
if (isolation === 'shared') {
|
|
61
116
|
// Keep one twin's stray async rejection / uncaught throw from killing every other twin in
|
|
62
117
|
// the shared process. (A thrown *request* handler is already contained by Bun.serve → 500.)
|
|
@@ -87,7 +142,11 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
87
142
|
};
|
|
88
143
|
try {
|
|
89
144
|
await parallelStartup(specs, async (spec) => {
|
|
90
|
-
const factory = await loadFactory(spec);
|
|
145
|
+
const { factory, byteStream, discovery } = await loadFactory(spec);
|
|
146
|
+
if (byteStream)
|
|
147
|
+
streams.push(spec.id);
|
|
148
|
+
if (discovery)
|
|
149
|
+
discoverable.push(spec.id);
|
|
91
150
|
const server = await factory({ ...(spec.sideOptions ?? {}), port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
92
151
|
// Record the handle before validating it, so rollback also owns a bad factory result.
|
|
93
152
|
servers.push(server);
|
|
@@ -109,6 +168,9 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
109
168
|
return {
|
|
110
169
|
isolation,
|
|
111
170
|
ports,
|
|
171
|
+
streams,
|
|
172
|
+
discoverable,
|
|
173
|
+
warm: () => warmEach(discoverable, (id) => warmInProcess(ports[id])),
|
|
112
174
|
stop: async () => {
|
|
113
175
|
// Every stop runs, even when a sibling hangs or throws. Host shutdown
|
|
114
176
|
// still resolves within its budget so the owning runtime can retire it.
|
|
@@ -135,7 +197,13 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
135
197
|
return;
|
|
136
198
|
}
|
|
137
199
|
ports[spec.id] = msg.port;
|
|
200
|
+
if (msg.byteStream === true)
|
|
201
|
+
streams.push(spec.id);
|
|
202
|
+
if (msg.discovery === true)
|
|
203
|
+
discoverable.push(spec.id);
|
|
138
204
|
ready = true;
|
|
205
|
+
// the same progress, and the same start time for the receipt (host-cli.ts), as a shared twin's start
|
|
206
|
+
onEvent({ twin: spec.id, kind: 'started', detail: `port ${msg.port} at ${Math.round(performance.now())} ms` });
|
|
139
207
|
resolveReady();
|
|
140
208
|
}
|
|
141
209
|
});
|
|
@@ -170,6 +238,28 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
170
238
|
return {
|
|
171
239
|
isolation,
|
|
172
240
|
ports,
|
|
241
|
+
streams,
|
|
242
|
+
discoverable,
|
|
243
|
+
// a worker twin is warmed in its own thread, where its server's handler is: asked, and answered once done
|
|
244
|
+
warm: () => warmEach(discoverable, (id) => new Promise((resolve) => {
|
|
245
|
+
const worker = workers.get(id);
|
|
246
|
+
if (!worker) {
|
|
247
|
+
resolve();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const done = () => { clearTimeout(timer); worker.off('message', warmed); worker.off('exit', done); resolve(); };
|
|
251
|
+
const warmed = (msg) => { if (msg?.type === 'warmed')
|
|
252
|
+
done(); };
|
|
253
|
+
const timer = setTimeout(done, WARM_TIMEOUT_MS);
|
|
254
|
+
worker.on('message', warmed);
|
|
255
|
+
worker.once('exit', done);
|
|
256
|
+
try {
|
|
257
|
+
worker.postMessage({ type: 'warm' });
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
done();
|
|
261
|
+
}
|
|
262
|
+
})),
|
|
173
263
|
stop: async () => {
|
|
174
264
|
stopping.value = true;
|
|
175
265
|
await Promise.all([...workers.values()].map((w) => w.terminate()));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type GoMysqlBinding } from './go-mysql-binding.js';
|
|
2
|
+
import * as managedOriginalInputs from '@volter/world-core/managed-original-inputs';
|
|
3
|
+
import type { NativeMariaDBService } from './native-mariadb-backing.js';
|
|
4
|
+
export interface InfraService {
|
|
5
|
+
kind: string;
|
|
6
|
+
hostPort: number;
|
|
7
|
+
nativeMariaDB?: NativeMariaDBService;
|
|
8
|
+
virtualTcpEndpoint?: {
|
|
9
|
+
version: 1;
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
targetPort: number;
|
|
13
|
+
};
|
|
14
|
+
connectionBindings?: Record<string, GoMysqlBinding & {
|
|
15
|
+
containerPort: number;
|
|
16
|
+
}>;
|
|
17
|
+
originalInputs?: managedOriginalInputs.OriginalInputReference;
|
|
18
|
+
sides?: Record<string, {
|
|
19
|
+
hostPort: number;
|
|
20
|
+
containerPort: number;
|
|
21
|
+
}>;
|
|
22
|
+
configuration?: Record<string, unknown>;
|
|
23
|
+
replicaSet?: string;
|
|
24
|
+
postgres?: {
|
|
25
|
+
user: string;
|
|
26
|
+
database: string;
|
|
27
|
+
password?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Read the generated definition's loopback port mappings and backing-independent options.
|
|
31
|
+
* The Compose extension carries a MongoDB replica-set name. Older generated commands remain readable. */
|
|
32
|
+
export declare function parseInfraDefinition(text: string): InfraService[];
|
|
33
|
+
/** Endpoints of the declared infrastructure, published through external.discover. Only URLs
|
|
34
|
+
* whose protocol and loopback port match a declared service belong to this lifecycle. */
|
|
35
|
+
export declare function infraConnections(services: InfraService[], env: Record<string, string | undefined>): Record<string, string>;
|