@volter/world-runtime 2.0.17 → 2.0.19
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/host-cli.js +4 -2
- package/dist/src/host-worker.js +1 -1
- package/dist/src/host.d.ts +3 -0
- package/dist/src/host.js +1 -1
- package/dist/src/init.d.ts +0 -3
- package/dist/src/init.js +0 -9
- package/dist/src/pack-facts.d.ts +6 -0
- package/dist/src/runtime.d.ts +12 -0
- package/dist/src/runtime.js +35 -1
- package/package.json +4 -4
- package/src/host-cli.ts +4 -2
- package/src/host-worker.ts +2 -2
- package/src/host.ts +5 -2
- package/src/init.ts +0 -9
- package/src/pack-facts.ts +2 -0
- package/src/runtime.ts +33 -1
package/dist/src/host-cli.js
CHANGED
|
@@ -28,11 +28,13 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
28
28
|
const [id, module, exportName, port, root, ...flags] = raw.split('|');
|
|
29
29
|
const ro = flags.find((f) => f === 'ro');
|
|
30
30
|
const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
|
|
31
|
+
// the World's managed database a pack declaring `managedDatabase` is bound to (runtime.ts, managedDatabaseFor)
|
|
32
|
+
const database = flags.find((f) => f.startsWith('database='))?.slice('database='.length);
|
|
31
33
|
if (!id || !module || !exportName || !port || !root) {
|
|
32
|
-
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>])\n`);
|
|
34
|
+
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>])\n`);
|
|
33
35
|
process.exit(2);
|
|
34
36
|
}
|
|
35
|
-
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}) });
|
|
37
|
+
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}) });
|
|
36
38
|
}
|
|
37
39
|
if (specs.length === 0) {
|
|
38
40
|
process.stderr.write('no --spec given\n');
|
package/dist/src/host-worker.js
CHANGED
|
@@ -11,6 +11,6 @@ if (typeof factory !== 'function') {
|
|
|
11
11
|
throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
|
|
12
12
|
}
|
|
13
13
|
// Awaited: an async factory (fly) must post `ready` only once its listener is bound.
|
|
14
|
-
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly });
|
|
14
|
+
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
15
15
|
parentPort?.postMessage({ type: 'ready', port: server.port });
|
|
16
16
|
// The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
|
package/dist/src/host.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export type ColocatedTwinSpec = {
|
|
|
11
11
|
/** The world's handlers file for a scenario-carrying (generative) twin — the colocate contract's
|
|
12
12
|
* scenarioPath (R2a): the same file the spawn path passes as --scenario. */
|
|
13
13
|
scenarioPath?: string;
|
|
14
|
+
/** The World's managed database a pack declaring `managedDatabase` serves over (runtime.ts, managedDatabaseFor):
|
|
15
|
+
* the colocated form of the spawn path's `serve <arg> <url>`. */
|
|
16
|
+
database?: string;
|
|
14
17
|
};
|
|
15
18
|
export type HostIsolation = 'shared' | 'worker';
|
|
16
19
|
export type ColocatedHost = {
|
package/dist/src/host.js
CHANGED
|
@@ -62,7 +62,7 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
62
62
|
const servers = [];
|
|
63
63
|
for (const spec of specs) {
|
|
64
64
|
const factory = await loadFactory(spec);
|
|
65
|
-
servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}) }));
|
|
65
|
+
servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) }));
|
|
66
66
|
}
|
|
67
67
|
return {
|
|
68
68
|
isolation,
|
package/dist/src/init.d.ts
CHANGED
|
@@ -184,9 +184,6 @@ export declare function packCatalog(root: string): string[];
|
|
|
184
184
|
* inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
|
|
185
185
|
* — inngest@4's own apiBaseUrl/eventBaseUrl getters.
|
|
186
186
|
* ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
|
|
187
|
-
* supabase `SUPABASE_MGMT_TWIN_URL` — the MANAGEMENT (control-plane) twin only,
|
|
188
|
-
* which is what this pack is; the DATA plane is the real local Supabase
|
|
189
|
-
* stack and is emitted as an infra stub, not faked.
|
|
190
187
|
* livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
|
|
191
188
|
* URL the SDK takes as its first constructor argument.
|
|
192
189
|
*
|
package/dist/src/init.js
CHANGED
|
@@ -63,9 +63,6 @@ export function packCatalog(root) {
|
|
|
63
63
|
* inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
|
|
64
64
|
* — inngest@4's own apiBaseUrl/eventBaseUrl getters.
|
|
65
65
|
* ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
|
|
66
|
-
* supabase `SUPABASE_MGMT_TWIN_URL` — the MANAGEMENT (control-plane) twin only,
|
|
67
|
-
* which is what this pack is; the DATA plane is the real local Supabase
|
|
68
|
-
* stack and is emitted as an infra stub, not faked.
|
|
69
66
|
* livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
|
|
70
67
|
* URL the SDK takes as its first constructor argument.
|
|
71
68
|
*
|
|
@@ -86,12 +83,6 @@ export const APP_READ_ENDPOINT_ENV = {
|
|
|
86
83
|
injectEnv: 'AI_GATEWAY_BASE_URL',
|
|
87
84
|
note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
|
|
88
85
|
},
|
|
89
|
-
supabase: {
|
|
90
|
-
injectEnv: 'SUPABASE_MGMT_TWIN_URL',
|
|
91
|
-
note: 'CONTROL PLANE ONLY — this twin is the Supabase MANAGEMENT API. The DATA plane (Postgres/PostgREST/Storage) '
|
|
92
|
-
+ 'is the REAL local Supabase stack: see the `//infra` supabase stub, fill in its `supabase start` lifecycle, and '
|
|
93
|
-
+ 'move it into "services". Until then the proof reports supabase UNINTERCEPTABLE, which is the truth.',
|
|
94
|
-
},
|
|
95
86
|
livekit: {
|
|
96
87
|
injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
|
|
97
88
|
note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
|
package/dist/src/pack-facts.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export type PackFacts = {
|
|
|
38
38
|
* declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
|
|
39
39
|
* `colocate:` service entries so a world's twins boot inside ONE host process (R2a). */
|
|
40
40
|
serveExport?: string;
|
|
41
|
+
/** The World's managed database the runtime binds the twin to at boot (TwinPack.managedDatabase). */
|
|
42
|
+
managedDatabase?: {
|
|
43
|
+
kind: 'postgres';
|
|
44
|
+
arg: string;
|
|
45
|
+
note: string;
|
|
46
|
+
};
|
|
41
47
|
/** R16: the protocol major the package targets and its standing against the kernel that compiled the artifact. */
|
|
42
48
|
protocol?: {
|
|
43
49
|
declared?: string;
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -75,6 +75,18 @@ export declare function sharingClaimAlive(claim: SharingClaim): boolean;
|
|
|
75
75
|
/** The process's environment as the world admits it: what `stripEnv` names stays outside. */
|
|
76
76
|
export declare function admittedEnv(strip: string[] | undefined, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
77
77
|
export declare function saveWorldInstance(instance: WorldInstance): void;
|
|
78
|
+
/**
|
|
79
|
+
* THE MANAGED-DATABASE BINDING (architecture.md, managed infrastructure: the data plane's declared exception): for a
|
|
80
|
+
* twin whose pack declares `managedDatabase`, the URL of the World's own managed postgres — the service its
|
|
81
|
+
* `world.infrastructure.yml` declares, reached at the loopback URL the World's environment gives it (the connection
|
|
82
|
+
* `volter-world-infra` publishes, infraConnections). Read from the config beside the definition, never from the
|
|
83
|
+
* caller's environment; a World that declares no managed postgres, or whose database URL points outside it, binds
|
|
84
|
+
* nothing (undefined), and the twin refuses its data plane.
|
|
85
|
+
*/
|
|
86
|
+
export declare function managedDatabaseFor(vendor: string, configPath: string | undefined, env: Record<string, string | undefined>): {
|
|
87
|
+
arg: string;
|
|
88
|
+
url: string;
|
|
89
|
+
} | undefined;
|
|
78
90
|
export declare function upWorld(configId: string, options?: UpWorldOptions, abort?: AbortSignal): Promise<WorldInstance>;
|
|
79
91
|
export declare function downWorld(name: string, root?: string, options?: {
|
|
80
92
|
graceMs?: number;
|
package/dist/src/runtime.js
CHANGED
|
@@ -15,6 +15,7 @@ import { constants as osConstants, hostname as osHostname } from 'node:os';
|
|
|
15
15
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, existsSync } from 'node:fs';
|
|
16
16
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
17
17
|
import { packFacts } from "./pack-facts.js";
|
|
18
|
+
import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
|
|
18
19
|
import { describeServiceEnd, readServiceEnd, serviceOutput } from "./service-exit.js";
|
|
19
20
|
import { Socket } from 'node:net';
|
|
20
21
|
import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core';
|
|
@@ -1035,6 +1036,11 @@ async function startService(config, service, worldEnv, paths, allocatedPorts, bo
|
|
|
1035
1036
|
const spawnedModule = (service.type ?? 'twin') === 'twin' ? spawnedTwinModule(service, paths.root) : undefined;
|
|
1036
1037
|
const mounted = spawnedModule !== undefined ? lockfileCheck(service, spawnedModule) : {};
|
|
1037
1038
|
const args = [...(service.args ?? [])];
|
|
1039
|
+
if ((service.type ?? 'twin') === 'twin') {
|
|
1040
|
+
const binding = managedDatabaseFor(vendorOfModule(spawnedModule, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
|
|
1041
|
+
if (binding !== undefined)
|
|
1042
|
+
args.push(binding.arg, binding.url);
|
|
1043
|
+
}
|
|
1038
1044
|
if (service.portArg !== false)
|
|
1039
1045
|
args.push(service.portArg ?? '--port', String(port));
|
|
1040
1046
|
if (service.rootArg !== false)
|
|
@@ -1185,6 +1191,33 @@ function spawnedTwinModule(service, root) {
|
|
|
1185
1191
|
const first = (service.args ?? []).find((a) => /packages\/twin\/[^/]+\//.test(a) || /@volter\/twin-[^/]+\//.test(a));
|
|
1186
1192
|
return first === undefined ? undefined : (first.startsWith('/') ? first : resolve(root, first));
|
|
1187
1193
|
}
|
|
1194
|
+
/** The vendor a twin module belongs to (`packages/twin/<vendor>/…` or `@volter/twin-<vendor>/…`), else the service id. */
|
|
1195
|
+
function vendorOfModule(modulePath, serviceId) {
|
|
1196
|
+
const m = modulePath === undefined ? null : (/\/packages\/twin\/([^/]+)\//.exec(modulePath) ?? /@volter\/twin-([^/]+)(\/|$)/.exec(modulePath));
|
|
1197
|
+
return m?.[1] ?? serviceId;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* THE MANAGED-DATABASE BINDING (architecture.md, managed infrastructure: the data plane's declared exception): for a
|
|
1201
|
+
* twin whose pack declares `managedDatabase`, the URL of the World's own managed postgres — the service its
|
|
1202
|
+
* `world.infrastructure.yml` declares, reached at the loopback URL the World's environment gives it (the connection
|
|
1203
|
+
* `volter-world-infra` publishes, infraConnections). Read from the config beside the definition, never from the
|
|
1204
|
+
* caller's environment; a World that declares no managed postgres, or whose database URL points outside it, binds
|
|
1205
|
+
* nothing (undefined), and the twin refuses its data plane.
|
|
1206
|
+
*/
|
|
1207
|
+
export function managedDatabaseFor(vendor, configPath, env) {
|
|
1208
|
+
const declared = packFacts()[vendor]?.managedDatabase;
|
|
1209
|
+
if (declared === undefined || configPath === undefined)
|
|
1210
|
+
return undefined;
|
|
1211
|
+
const definition = join(dirname(configPath), 'world.infrastructure.yml');
|
|
1212
|
+
if (!existsSync(definition))
|
|
1213
|
+
return undefined;
|
|
1214
|
+
const services = parseInfraDefinition(readFileSync(definition, 'utf8')).filter((service) => service.kind === declared.kind);
|
|
1215
|
+
if (services.length === 0)
|
|
1216
|
+
return undefined;
|
|
1217
|
+
const connections = infraConnections(services, env);
|
|
1218
|
+
const url = Object.keys(connections).sort().map((name) => connections[name])[0];
|
|
1219
|
+
return url === undefined ? undefined : { arg: declared.arg, url };
|
|
1220
|
+
}
|
|
1188
1221
|
/**
|
|
1189
1222
|
* The config as the runtime boots it (docs/concepts/worlds.md#the-config-and-the-running-world): a service
|
|
1190
1223
|
* naming a `package` gets its entry and colocate module resolved from the world root NOW, by
|
|
@@ -1247,7 +1280,8 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1247
1280
|
// catalog recorded for the package (pack-facts), refused two majors back, warned one back.
|
|
1248
1281
|
const mounted = lockfileCheck(service, module);
|
|
1249
1282
|
const scenario = service.colocate.scenarioPath;
|
|
1250
|
-
|
|
1283
|
+
const binding = managedDatabaseFor(vendorOfModule(module, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
|
|
1284
|
+
args.push('--spec', [service.id, module, service.colocate.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : []), ...(binding !== undefined ? [`database=${binding.url}`] : [])].join('|'));
|
|
1251
1285
|
allocated.push({ service, port, mounted });
|
|
1252
1286
|
}
|
|
1253
1287
|
const log = join(paths.logs, 'host.log');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"node": ">=22.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@volter/world-core": "2.0.
|
|
70
|
+
"@volter/world-core": "2.0.12",
|
|
71
71
|
"@volter/world-console": "2.0.9"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@electric-sql/pglite": "0.5.8",
|
|
75
75
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
76
76
|
"@volter/world-access": "2.0.1",
|
|
77
|
-
"@volter/world-core": "2.0.
|
|
77
|
+
"@volter/world-core": "2.0.12",
|
|
78
78
|
"pg-gateway": "0.3.0-beta.4",
|
|
79
79
|
"smol-toml": "^1.8.0"
|
|
80
80
|
},
|
|
@@ -84,6 +84,6 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"optionalDependencies": {
|
|
87
|
-
"@volter/twin-mongodb": "0.1.
|
|
87
|
+
"@volter/twin-mongodb": "0.1.12"
|
|
88
88
|
}
|
|
89
89
|
}
|
package/src/host-cli.ts
CHANGED
|
@@ -30,11 +30,13 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
30
30
|
const [id, module, exportName, port, root, ...flags] = raw.split('|');
|
|
31
31
|
const ro = flags.find((f) => f === 'ro');
|
|
32
32
|
const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
|
|
33
|
+
// the World's managed database a pack declaring `managedDatabase` is bound to (runtime.ts, managedDatabaseFor)
|
|
34
|
+
const database = flags.find((f) => f.startsWith('database='))?.slice('database='.length);
|
|
33
35
|
if (!id || !module || !exportName || !port || !root) {
|
|
34
|
-
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>])\n`);
|
|
36
|
+
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>])\n`);
|
|
35
37
|
process.exit(2);
|
|
36
38
|
}
|
|
37
|
-
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}) });
|
|
39
|
+
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}) });
|
|
38
40
|
}
|
|
39
41
|
if (specs.length === 0) { process.stderr.write('no --spec given\n'); process.exit(2); }
|
|
40
42
|
|
package/src/host-worker.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { importModule } from './import-module.ts';
|
|
|
9
9
|
const spec = workerData as ColocatedTwinSpec;
|
|
10
10
|
|
|
11
11
|
type TwinServer = { port: number; stop: () => void | Promise<void> };
|
|
12
|
-
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean }) => TwinServer | Promise<TwinServer>;
|
|
12
|
+
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; database?: string }) => TwinServer | Promise<TwinServer>;
|
|
13
13
|
|
|
14
14
|
const mod = await importModule(spec.module);
|
|
15
15
|
const factory = mod[spec.export] as TwinServerFactory | undefined;
|
|
@@ -18,7 +18,7 @@ if (typeof factory !== 'function') {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Awaited: an async factory (fly) must post `ready` only once its listener is bound.
|
|
21
|
-
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly });
|
|
21
|
+
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
22
22
|
parentPort?.postMessage({ type: 'ready', port: server.port });
|
|
23
23
|
|
|
24
24
|
// The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
|
package/src/host.ts
CHANGED
|
@@ -37,6 +37,9 @@ export type ColocatedTwinSpec = {
|
|
|
37
37
|
/** The world's handlers file for a scenario-carrying (generative) twin — the colocate contract's
|
|
38
38
|
* scenarioPath (R2a): the same file the spawn path passes as --scenario. */
|
|
39
39
|
scenarioPath?: string;
|
|
40
|
+
/** The World's managed database a pack declaring `managedDatabase` serves over (runtime.ts, managedDatabaseFor):
|
|
41
|
+
* the colocated form of the spawn path's `serve <arg> <url>`. */
|
|
42
|
+
database?: string;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
export type HostIsolation = 'shared' | 'worker';
|
|
@@ -55,7 +58,7 @@ export type ColocatedHost = {
|
|
|
55
58
|
// made host.stop() throw `s.stop is not a function`, turning every SIGTERM into downWorld's
|
|
56
59
|
// SIGKILL escalation for the whole world).
|
|
57
60
|
type TwinServer = { port: number; stop: () => void | Promise<void> };
|
|
58
|
-
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; scenarioPath?: string }) => TwinServer | Promise<TwinServer>;
|
|
61
|
+
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; scenarioPath?: string; database?: string }) => TwinServer | Promise<TwinServer>;
|
|
59
62
|
|
|
60
63
|
async function loadFactory(spec: ColocatedTwinSpec): Promise<TwinServerFactory> {
|
|
61
64
|
const mod = await importModule(spec.module);
|
|
@@ -103,7 +106,7 @@ export async function startColocatedHost(
|
|
|
103
106
|
const servers: TwinServer[] = [];
|
|
104
107
|
for (const spec of specs) {
|
|
105
108
|
const factory = await loadFactory(spec);
|
|
106
|
-
servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}) }));
|
|
109
|
+
servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) }));
|
|
107
110
|
}
|
|
108
111
|
return {
|
|
109
112
|
isolation,
|
package/src/init.ts
CHANGED
|
@@ -244,9 +244,6 @@ export function packCatalog(root: string): string[] {
|
|
|
244
244
|
* inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
|
|
245
245
|
* — inngest@4's own apiBaseUrl/eventBaseUrl getters.
|
|
246
246
|
* ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
|
|
247
|
-
* supabase `SUPABASE_MGMT_TWIN_URL` — the MANAGEMENT (control-plane) twin only,
|
|
248
|
-
* which is what this pack is; the DATA plane is the real local Supabase
|
|
249
|
-
* stack and is emitted as an infra stub, not faked.
|
|
250
247
|
* livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
|
|
251
248
|
* URL the SDK takes as its first constructor argument.
|
|
252
249
|
*
|
|
@@ -267,12 +264,6 @@ export const APP_READ_ENDPOINT_ENV: Record<string, { injectEnv?: string; injectE
|
|
|
267
264
|
injectEnv: 'AI_GATEWAY_BASE_URL',
|
|
268
265
|
note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
|
|
269
266
|
},
|
|
270
|
-
supabase: {
|
|
271
|
-
injectEnv: 'SUPABASE_MGMT_TWIN_URL',
|
|
272
|
-
note: 'CONTROL PLANE ONLY — this twin is the Supabase MANAGEMENT API. The DATA plane (Postgres/PostgREST/Storage) '
|
|
273
|
-
+ 'is the REAL local Supabase stack: see the `//infra` supabase stub, fill in its `supabase start` lifecycle, and '
|
|
274
|
-
+ 'move it into "services". Until then the proof reports supabase UNINTERCEPTABLE, which is the truth.',
|
|
275
|
-
},
|
|
276
267
|
livekit: {
|
|
277
268
|
injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
|
|
278
269
|
note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
|
package/src/pack-facts.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type PackFacts = {
|
|
|
25
25
|
* declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
|
|
26
26
|
* `colocate:` service entries so a world's twins boot inside ONE host process (R2a). */
|
|
27
27
|
serveExport?: string;
|
|
28
|
+
/** The World's managed database the runtime binds the twin to at boot (TwinPack.managedDatabase). */
|
|
29
|
+
managedDatabase?: { kind: 'postgres'; arg: string; note: string };
|
|
28
30
|
/** R16: the protocol major the package targets and its standing against the kernel that compiled the artifact. */
|
|
29
31
|
protocol?: { declared?: string; major: number; standing: 'current' | 'deprecated' | 'refused' | 'assumed' };
|
|
30
32
|
};
|
package/src/runtime.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { constants as osConstants, hostname as osHostname } from 'node:os';
|
|
|
15
15
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, existsSync } from 'node:fs';
|
|
16
16
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
17
17
|
import { packFacts } from './pack-facts.ts';
|
|
18
|
+
import { infraConnections, parseInfraDefinition } from './pglite-backing.ts';
|
|
18
19
|
import { describeServiceEnd, readServiceEnd, serviceOutput } from './service-exit.ts';
|
|
19
20
|
import { Socket } from 'node:net';
|
|
20
21
|
import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core';
|
|
@@ -1073,6 +1074,10 @@ async function startService(
|
|
|
1073
1074
|
const mounted: MountedPackage = spawnedModule !== undefined ? lockfileCheck(service, spawnedModule) : {};
|
|
1074
1075
|
|
|
1075
1076
|
const args = [...(service.args ?? [])];
|
|
1077
|
+
if ((service.type ?? 'twin') === 'twin') {
|
|
1078
|
+
const binding = managedDatabaseFor(vendorOfModule(spawnedModule, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
|
|
1079
|
+
if (binding !== undefined) args.push(binding.arg, binding.url);
|
|
1080
|
+
}
|
|
1076
1081
|
if (service.portArg !== false) args.push(service.portArg ?? '--port', String(port));
|
|
1077
1082
|
if (service.rootArg !== false) args.push(service.rootArg ?? '--root', serviceDataDir);
|
|
1078
1083
|
|
|
@@ -1205,6 +1210,32 @@ function spawnedTwinModule(service: WorldServiceConfig, root: string): string |
|
|
|
1205
1210
|
return first === undefined ? undefined : (first.startsWith('/') ? first : resolve(root, first));
|
|
1206
1211
|
}
|
|
1207
1212
|
|
|
1213
|
+
/** The vendor a twin module belongs to (`packages/twin/<vendor>/…` or `@volter/twin-<vendor>/…`), else the service id. */
|
|
1214
|
+
function vendorOfModule(modulePath: string | undefined, serviceId: string): string {
|
|
1215
|
+
const m = modulePath === undefined ? null : (/\/packages\/twin\/([^/]+)\//.exec(modulePath) ?? /@volter\/twin-([^/]+)(\/|$)/.exec(modulePath));
|
|
1216
|
+
return m?.[1] ?? serviceId;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* THE MANAGED-DATABASE BINDING (architecture.md, managed infrastructure: the data plane's declared exception): for a
|
|
1221
|
+
* twin whose pack declares `managedDatabase`, the URL of the World's own managed postgres — the service its
|
|
1222
|
+
* `world.infrastructure.yml` declares, reached at the loopback URL the World's environment gives it (the connection
|
|
1223
|
+
* `volter-world-infra` publishes, infraConnections). Read from the config beside the definition, never from the
|
|
1224
|
+
* caller's environment; a World that declares no managed postgres, or whose database URL points outside it, binds
|
|
1225
|
+
* nothing (undefined), and the twin refuses its data plane.
|
|
1226
|
+
*/
|
|
1227
|
+
export function managedDatabaseFor(vendor: string, configPath: string | undefined, env: Record<string, string | undefined>): { arg: string; url: string } | undefined {
|
|
1228
|
+
const declared = packFacts()[vendor]?.managedDatabase;
|
|
1229
|
+
if (declared === undefined || configPath === undefined) return undefined;
|
|
1230
|
+
const definition = join(dirname(configPath), 'world.infrastructure.yml');
|
|
1231
|
+
if (!existsSync(definition)) return undefined;
|
|
1232
|
+
const services = parseInfraDefinition(readFileSync(definition, 'utf8')).filter((service) => service.kind === declared.kind);
|
|
1233
|
+
if (services.length === 0) return undefined;
|
|
1234
|
+
const connections = infraConnections(services, env);
|
|
1235
|
+
const url = Object.keys(connections).sort().map((name) => connections[name]!)[0];
|
|
1236
|
+
return url === undefined ? undefined : { arg: declared.arg, url };
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1208
1239
|
/**
|
|
1209
1240
|
* The config as the runtime boots it (docs/concepts/worlds.md#the-config-and-the-running-world): a service
|
|
1210
1241
|
* naming a `package` gets its entry and colocate module resolved from the world root NOW, by
|
|
@@ -1272,7 +1303,8 @@ async function startColocatedServices(
|
|
|
1272
1303
|
// catalog recorded for the package (pack-facts), refused two majors back, warned one back.
|
|
1273
1304
|
const mounted = lockfileCheck(service, module);
|
|
1274
1305
|
const scenario = service.colocate!.scenarioPath;
|
|
1275
|
-
|
|
1306
|
+
const binding = managedDatabaseFor(vendorOfModule(module, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
|
|
1307
|
+
args.push('--spec', [service.id, module, service.colocate!.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : []), ...(binding !== undefined ? [`database=${binding.url}`] : [])].join('|'));
|
|
1276
1308
|
allocated.push({ service, port, mounted });
|
|
1277
1309
|
}
|
|
1278
1310
|
|