@volter/world-runtime 2.0.48 → 2.0.50
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/runtime.js +67 -50
- package/package.json +4 -4
- package/src/runtime.ts +54 -38
package/dist/src/runtime.js
CHANGED
|
@@ -1342,7 +1342,7 @@ function versionSatisfies(actual, pin) {
|
|
|
1342
1342
|
*/
|
|
1343
1343
|
async function startColocatedServices(config, colocated, isolation, worldEnv, paths, allocatedPorts, boot) {
|
|
1344
1344
|
if (colocated.length === 0)
|
|
1345
|
-
return [];
|
|
1345
|
+
return { services: [], ready: Promise.resolve() };
|
|
1346
1346
|
const hostCli = siblingScript(import.meta.url, 'host-cli');
|
|
1347
1347
|
const readyFile = join(paths.instance, `host-ready-${boot.bootId}.json`);
|
|
1348
1348
|
const args = [hostCli, '--isolation', isolation === 'colocated' ? 'shared' : 'worker', '--ready-file', readyFile];
|
|
@@ -1381,55 +1381,10 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1381
1381
|
exitState.value = { code, signal };
|
|
1382
1382
|
});
|
|
1383
1383
|
const command = [process.execPath, ...args];
|
|
1384
|
-
const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
|
|
1385
|
-
await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1386
|
-
if (undeadPids([child.pid]).length)
|
|
1387
|
-
recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
|
|
1388
|
-
try {
|
|
1389
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
1390
|
-
}
|
|
1391
|
-
catch { /* best effort cleanup */ }
|
|
1392
|
-
throw new Error(`Co-located host failed to complete startup (isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1393
|
-
});
|
|
1394
|
-
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1395
|
-
const bindTimeoutMs = typeof service.ready === 'object' && service.ready.timeoutMs !== undefined ? Math.max(service.ready.timeoutMs, 15_000) : 15_000;
|
|
1396
|
-
await untilReadyOrEnded((abort) => waitForTcp(port, bindTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1397
|
-
if (undeadPids([child.pid]).length)
|
|
1398
|
-
recordWorldStop([log], 'world.service.stop', 'up (startup failed)', 'SIGTERM');
|
|
1399
|
-
try {
|
|
1400
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
1401
|
-
}
|
|
1402
|
-
catch { /* best effort cleanup */ }
|
|
1403
|
-
throw new Error(`Co-located twin "${service.id}" failed to start (host isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1404
|
-
});
|
|
1405
|
-
});
|
|
1406
|
-
let hostSettleTimer;
|
|
1407
|
-
const hostSettle = new Promise((resolveAlive) => { hostSettleTimer = setTimeout(() => resolveAlive('alive'), 300); });
|
|
1408
|
-
try {
|
|
1409
|
-
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1410
|
-
// The host has completed every start. Each matching boot answer can now
|
|
1411
|
-
// end its own settle; legacy twins share one full host guard.
|
|
1412
|
-
await confirmOwnPort(port, boot, ended, true, `Co-located twin "${service.id}"`, log, true, hostSettle).catch((error) => {
|
|
1413
|
-
if (undeadPids([child.pid]).length)
|
|
1414
|
-
recordWorldStop([log], 'world.service.stop', 'up (port not its own)', 'SIGTERM');
|
|
1415
|
-
try {
|
|
1416
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
1417
|
-
}
|
|
1418
|
-
catch { /* best effort cleanup */ }
|
|
1419
|
-
throw error;
|
|
1420
|
-
});
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
finally {
|
|
1424
|
-
if (hostSettleTimer !== undefined)
|
|
1425
|
-
clearTimeout(hostSettleTimer);
|
|
1426
|
-
}
|
|
1427
|
-
if (exitState.value !== null) {
|
|
1428
|
-
throw new Error(`The co-located host ended during startup even though its ports answer, so other/stale processes probably hold them: the host ${endedAccount(log)}\n`
|
|
1429
|
-
+ `Stop leftover services (e.g. \`volter-world down\`) and retry.\nLog: ${log}`);
|
|
1430
|
-
}
|
|
1431
1384
|
const host = '127.0.0.1';
|
|
1432
|
-
|
|
1385
|
+
// Every twin's address is known once its port is allocated: the records and
|
|
1386
|
+
// the environment they inject exist before the host reports its starts.
|
|
1387
|
+
const services = allocated.map(({ service, port, mounted }) => {
|
|
1433
1388
|
const url = `http://${host}:${port}`;
|
|
1434
1389
|
return {
|
|
1435
1390
|
id: service.id,
|
|
@@ -1448,6 +1403,55 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1448
1403
|
},
|
|
1449
1404
|
};
|
|
1450
1405
|
});
|
|
1406
|
+
const ready = (async () => {
|
|
1407
|
+
const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
|
|
1408
|
+
await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1409
|
+
if (undeadPids([child.pid]).length)
|
|
1410
|
+
recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
|
|
1411
|
+
try {
|
|
1412
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
1413
|
+
}
|
|
1414
|
+
catch { /* best effort cleanup */ }
|
|
1415
|
+
throw new Error(`Co-located host failed to complete startup (isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1416
|
+
});
|
|
1417
|
+
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1418
|
+
const bindTimeoutMs = typeof service.ready === 'object' && service.ready.timeoutMs !== undefined ? Math.max(service.ready.timeoutMs, 15_000) : 15_000;
|
|
1419
|
+
await untilReadyOrEnded((abort) => waitForTcp(port, bindTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1420
|
+
if (undeadPids([child.pid]).length)
|
|
1421
|
+
recordWorldStop([log], 'world.service.stop', 'up (startup failed)', 'SIGTERM');
|
|
1422
|
+
try {
|
|
1423
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
1424
|
+
}
|
|
1425
|
+
catch { /* best effort cleanup */ }
|
|
1426
|
+
throw new Error(`Co-located twin "${service.id}" failed to start (host isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
// The host's receipt (this boot, every requested port) is published only
|
|
1430
|
+
// after every factory bound its own port; a port another process held fails
|
|
1431
|
+
// its bind and leaves no receipt. The receipt is the host's ownership proof
|
|
1432
|
+
// for each port, so no twin waits out a settle (ADR-0008). A port that
|
|
1433
|
+
// answers with another boot's identity is still refused.
|
|
1434
|
+
const hostSettle = Promise.resolve('alive');
|
|
1435
|
+
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1436
|
+
await confirmOwnPort(port, boot, ended, true, `Co-located twin "${service.id}"`, log, true, hostSettle).catch((error) => {
|
|
1437
|
+
if (undeadPids([child.pid]).length)
|
|
1438
|
+
recordWorldStop([log], 'world.service.stop', 'up (port not its own)', 'SIGTERM');
|
|
1439
|
+
try {
|
|
1440
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
1441
|
+
}
|
|
1442
|
+
catch { /* best effort cleanup */ }
|
|
1443
|
+
throw error;
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
if (exitState.value !== null) {
|
|
1447
|
+
throw new Error(`The co-located host ended during startup even though its ports answer, so other/stale processes probably hold them: the host ${endedAccount(log)}\n`
|
|
1448
|
+
+ `Stop leftover services (e.g. \`volter-world down\`) and retry.\nLog: ${log}`);
|
|
1449
|
+
}
|
|
1450
|
+
})();
|
|
1451
|
+
// Observed here so a failure before the caller awaits it is not unhandled;
|
|
1452
|
+
// the caller awaits `ready` before the World is up.
|
|
1453
|
+
ready.catch(() => undefined);
|
|
1454
|
+
return { services, ready };
|
|
1451
1455
|
}
|
|
1452
1456
|
export async function upWorld(configId, options = {}, abort) {
|
|
1453
1457
|
abort?.throwIfAborted();
|
|
@@ -1698,12 +1702,19 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1698
1702
|
const allocatedPorts = new Set(loaded.config.services.flatMap((service) => typeof service.port === 'number' ? [service.port] : []));
|
|
1699
1703
|
// Non-'process' isolation: boot every colocate-declaring service inside ONE host child
|
|
1700
1704
|
// first; anything without `colocate` (and externals) still goes through the spawn path.
|
|
1705
|
+
// The co-located host's twins are known by address once their ports are
|
|
1706
|
+
// allocated. A twin serves and calls no other service while it starts,
|
|
1707
|
+
// so a later twin declaration starts beside the host's startup; any other
|
|
1708
|
+
// service waits until every earlier one is ready, as declared (ADR-0009).
|
|
1709
|
+
let colocatedReady;
|
|
1701
1710
|
if (isolation !== 'process') {
|
|
1702
1711
|
const colocated = loaded.config.services.filter((service) => service.colocate && service.type !== 'external');
|
|
1703
|
-
|
|
1712
|
+
const host = await startColocatedServices(loaded.config, colocated, isolation, env, { root, instance, logs, data }, allocatedPorts, boot);
|
|
1713
|
+
for (const started of host.services) {
|
|
1704
1714
|
services[started.id] = started;
|
|
1705
1715
|
Object.assign(env, started.env);
|
|
1706
1716
|
}
|
|
1717
|
+
colocatedReady = host.ready;
|
|
1707
1718
|
recordPidsSoFar();
|
|
1708
1719
|
writeProxyEnv(root, name, env);
|
|
1709
1720
|
}
|
|
@@ -1736,6 +1747,10 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1736
1747
|
for (const service of loaded.config.services) {
|
|
1737
1748
|
if (services[service.id])
|
|
1738
1749
|
continue; // already up co-located
|
|
1750
|
+
if (colocatedReady && (service.type ?? 'twin') !== 'twin') {
|
|
1751
|
+
await colocatedReady;
|
|
1752
|
+
colocatedReady = undefined;
|
|
1753
|
+
}
|
|
1739
1754
|
attemptProxy();
|
|
1740
1755
|
boot.signal.throwIfAborted();
|
|
1741
1756
|
const started = await startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot);
|
|
@@ -1744,6 +1759,8 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1744
1759
|
Object.assign(env, started.env);
|
|
1745
1760
|
writeProxyEnv(root, name, env);
|
|
1746
1761
|
}
|
|
1762
|
+
if (colocatedReady)
|
|
1763
|
+
await colocatedReady;
|
|
1747
1764
|
detachBootSignals();
|
|
1748
1765
|
// Re-evaluate once more after the LAST service: a single/last-twin world's OWN vendor twin
|
|
1749
1766
|
// only lands in `env` once IT has started, so a check that only runs "before starting the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.50",
|
|
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.32",
|
|
71
71
|
"@volter/world-console": "2.0.13"
|
|
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.2",
|
|
77
|
-
"@volter/world-core": "2.0.
|
|
77
|
+
"@volter/world-core": "2.0.32",
|
|
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.32"
|
|
88
88
|
}
|
|
89
89
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -1347,8 +1347,8 @@ async function startColocatedServices(
|
|
|
1347
1347
|
paths: { root: string; instance: string; logs: string; data: string },
|
|
1348
1348
|
allocatedPorts: Set<number>,
|
|
1349
1349
|
boot: BootContext,
|
|
1350
|
-
): Promise<WorldServiceInstance[]> {
|
|
1351
|
-
if (colocated.length === 0) return [];
|
|
1350
|
+
): Promise<{ services: WorldServiceInstance[]; ready: Promise<void> }> {
|
|
1351
|
+
if (colocated.length === 0) return { services: [], ready: Promise.resolve() };
|
|
1352
1352
|
const hostCli = siblingScript(import.meta.url, 'host-cli');
|
|
1353
1353
|
const readyFile = join(paths.instance, `host-ready-${boot.bootId}.json`);
|
|
1354
1354
|
const args: string[] = [hostCli, '--isolation', isolation === 'colocated' ? 'shared' : 'worker', '--ready-file', readyFile];
|
|
@@ -1389,42 +1389,10 @@ async function startColocatedServices(
|
|
|
1389
1389
|
});
|
|
1390
1390
|
|
|
1391
1391
|
const command = [process.execPath, ...args];
|
|
1392
|
-
const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
|
|
1393
|
-
await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1394
|
-
if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
|
|
1395
|
-
try { process.kill(-child.pid!, 'SIGTERM'); } catch { /* best effort cleanup */ }
|
|
1396
|
-
throw new Error(`Co-located host failed to complete startup (isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1397
|
-
});
|
|
1398
|
-
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1399
|
-
const bindTimeoutMs = typeof service.ready === 'object' && service.ready.timeoutMs !== undefined ? Math.max(service.ready.timeoutMs, 15_000) : 15_000;
|
|
1400
|
-
await untilReadyOrEnded((abort) => waitForTcp(port, bindTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1401
|
-
if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (startup failed)', 'SIGTERM');
|
|
1402
|
-
try { process.kill(-child.pid!, 'SIGTERM'); } catch { /* best effort cleanup */ }
|
|
1403
|
-
throw new Error(`Co-located twin "${service.id}" failed to start (host isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1404
|
-
});
|
|
1405
|
-
});
|
|
1406
|
-
let hostSettleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1407
|
-
const hostSettle = new Promise<'alive'>((resolveAlive) => { hostSettleTimer = setTimeout(() => resolveAlive('alive'), 300); });
|
|
1408
|
-
try {
|
|
1409
|
-
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1410
|
-
// The host has completed every start. Each matching boot answer can now
|
|
1411
|
-
// end its own settle; legacy twins share one full host guard.
|
|
1412
|
-
await confirmOwnPort(port, boot, ended, true, `Co-located twin "${service.id}"`, log, true, hostSettle).catch((error) => {
|
|
1413
|
-
if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (port not its own)', 'SIGTERM');
|
|
1414
|
-
try { process.kill(-child.pid!, 'SIGTERM'); } catch { /* best effort cleanup */ }
|
|
1415
|
-
throw error;
|
|
1416
|
-
});
|
|
1417
|
-
});
|
|
1418
|
-
} finally { if (hostSettleTimer !== undefined) clearTimeout(hostSettleTimer); }
|
|
1419
|
-
if (exitState.value !== null) {
|
|
1420
|
-
throw new Error(
|
|
1421
|
-
`The co-located host ended during startup even though its ports answer, so other/stale processes probably hold them: the host ${endedAccount(log)}\n`
|
|
1422
|
-
+ `Stop leftover services (e.g. \`volter-world down\`) and retry.\nLog: ${log}`,
|
|
1423
|
-
);
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
1392
|
const host = '127.0.0.1';
|
|
1427
|
-
|
|
1393
|
+
// Every twin's address is known once its port is allocated: the records and
|
|
1394
|
+
// the environment they inject exist before the host reports its starts.
|
|
1395
|
+
const services = allocated.map(({ service, port, mounted }) => {
|
|
1428
1396
|
const url = `http://${host}:${port}`;
|
|
1429
1397
|
return {
|
|
1430
1398
|
id: service.id,
|
|
@@ -1443,6 +1411,45 @@ async function startColocatedServices(
|
|
|
1443
1411
|
},
|
|
1444
1412
|
};
|
|
1445
1413
|
});
|
|
1414
|
+
const ready = (async (): Promise<void> => {
|
|
1415
|
+
const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
|
|
1416
|
+
await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1417
|
+
if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (host startup incomplete)', 'SIGTERM');
|
|
1418
|
+
try { process.kill(-child.pid!, 'SIGTERM'); } catch { /* best effort cleanup */ }
|
|
1419
|
+
throw new Error(`Co-located host failed to complete startup (isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1420
|
+
});
|
|
1421
|
+
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1422
|
+
const bindTimeoutMs = typeof service.ready === 'object' && service.ready.timeoutMs !== undefined ? Math.max(service.ready.timeoutMs, 15_000) : 15_000;
|
|
1423
|
+
await untilReadyOrEnded((abort) => waitForTcp(port, bindTimeoutMs, abort), ended, boot, 'the co-located host', log).catch((error) => {
|
|
1424
|
+
if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (startup failed)', 'SIGTERM');
|
|
1425
|
+
try { process.kill(-child.pid!, 'SIGTERM'); } catch { /* best effort cleanup */ }
|
|
1426
|
+
throw new Error(`Co-located twin "${service.id}" failed to start (host isolation=${isolation}): ${error instanceof Error ? error.message : String(error)}\nLog: ${log}`);
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
// The host's receipt (this boot, every requested port) is published only
|
|
1430
|
+
// after every factory bound its own port; a port another process held fails
|
|
1431
|
+
// its bind and leaves no receipt. The receipt is the host's ownership proof
|
|
1432
|
+
// for each port, so no twin waits out a settle (ADR-0008). A port that
|
|
1433
|
+
// answers with another boot's identity is still refused.
|
|
1434
|
+
const hostSettle = Promise.resolve('alive' as const);
|
|
1435
|
+
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1436
|
+
await confirmOwnPort(port, boot, ended, true, `Co-located twin "${service.id}"`, log, true, hostSettle).catch((error) => {
|
|
1437
|
+
if (undeadPids([child.pid!]).length) recordWorldStop([log], 'world.service.stop', 'up (port not its own)', 'SIGTERM');
|
|
1438
|
+
try { process.kill(-child.pid!, 'SIGTERM'); } catch { /* best effort cleanup */ }
|
|
1439
|
+
throw error;
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
if (exitState.value !== null) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
`The co-located host ended during startup even though its ports answer, so other/stale processes probably hold them: the host ${endedAccount(log)}\n`
|
|
1445
|
+
+ `Stop leftover services (e.g. \`volter-world down\`) and retry.\nLog: ${log}`,
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
})();
|
|
1449
|
+
// Observed here so a failure before the caller awaits it is not unhandled;
|
|
1450
|
+
// the caller awaits `ready` before the World is up.
|
|
1451
|
+
ready.catch(() => undefined);
|
|
1452
|
+
return { services, ready };
|
|
1446
1453
|
}
|
|
1447
1454
|
|
|
1448
1455
|
export async function upWorld(configId: string, options: UpWorldOptions = {}, abort?: AbortSignal): Promise<WorldInstance> {
|
|
@@ -1669,12 +1676,19 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1669
1676
|
const allocatedPorts = new Set(loaded.config.services.flatMap((service) => typeof service.port === 'number' ? [service.port] : []));
|
|
1670
1677
|
// Non-'process' isolation: boot every colocate-declaring service inside ONE host child
|
|
1671
1678
|
// first; anything without `colocate` (and externals) still goes through the spawn path.
|
|
1679
|
+
// The co-located host's twins are known by address once their ports are
|
|
1680
|
+
// allocated. A twin serves and calls no other service while it starts,
|
|
1681
|
+
// so a later twin declaration starts beside the host's startup; any other
|
|
1682
|
+
// service waits until every earlier one is ready, as declared (ADR-0009).
|
|
1683
|
+
let colocatedReady: Promise<void> | undefined;
|
|
1672
1684
|
if (isolation !== 'process') {
|
|
1673
1685
|
const colocated = loaded.config.services.filter((service) => service.colocate && service.type !== 'external');
|
|
1674
|
-
|
|
1686
|
+
const host = await startColocatedServices(loaded.config, colocated, isolation, env, { root, instance, logs, data }, allocatedPorts, boot);
|
|
1687
|
+
for (const started of host.services) {
|
|
1675
1688
|
services[started.id] = started;
|
|
1676
1689
|
Object.assign(env, started.env);
|
|
1677
1690
|
}
|
|
1691
|
+
colocatedReady = host.ready;
|
|
1678
1692
|
recordPidsSoFar();
|
|
1679
1693
|
writeProxyEnv(root, name, env);
|
|
1680
1694
|
}
|
|
@@ -1706,6 +1720,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1706
1720
|
};
|
|
1707
1721
|
for (const service of loaded.config.services) {
|
|
1708
1722
|
if (services[service.id]) continue; // already up co-located
|
|
1723
|
+
if (colocatedReady && (service.type ?? 'twin') !== 'twin') { await colocatedReady; colocatedReady = undefined; }
|
|
1709
1724
|
attemptProxy();
|
|
1710
1725
|
boot.signal.throwIfAborted();
|
|
1711
1726
|
const started = await startService(loaded.config, service, env, { root, instance, logs, data }, allocatedPorts, boot);
|
|
@@ -1714,6 +1729,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1714
1729
|
Object.assign(env, started.env);
|
|
1715
1730
|
writeProxyEnv(root, name, env);
|
|
1716
1731
|
}
|
|
1732
|
+
if (colocatedReady) await colocatedReady;
|
|
1717
1733
|
detachBootSignals();
|
|
1718
1734
|
// Re-evaluate once more after the LAST service: a single/last-twin world's OWN vendor twin
|
|
1719
1735
|
// only lands in `env` once IT has started, so a check that only runs "before starting the
|