@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.
@@ -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
- return allocated.map(({ service, port, mounted }) => {
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
- for (const started of await startColocatedServices(loaded.config, colocated, isolation, env, { root, instance, logs, data }, allocatedPorts, boot)) {
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.48",
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.30",
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.30",
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.30"
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
- return allocated.map(({ service, port, mounted }) => {
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
- for (const started of await startColocatedServices(loaded.config, colocated, isolation, env, { root, instance, logs, data }, allocatedPorts, boot)) {
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