@volter/world-runtime 3.0.163 → 3.0.165

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.
Files changed (137) hide show
  1. package/dist/src/ancestry.js +2 -2
  2. package/dist/src/app-url.d.ts +5 -1
  3. package/dist/src/app-url.js +14 -1
  4. package/dist/src/branch.js +28 -8
  5. package/dist/src/browser-files.d.ts +5 -0
  6. package/dist/src/browser-files.js +14 -0
  7. package/dist/src/browsers.d.ts +2 -3
  8. package/dist/src/browsers.js +4 -4
  9. package/dist/src/catalog.d.ts +2 -1
  10. package/dist/src/catalog.js +9 -3
  11. package/dist/src/changeset.js +1 -1
  12. package/dist/src/cli-commands.js +71 -18
  13. package/dist/src/cli-load-clock.d.ts +1 -0
  14. package/dist/src/cli-load-clock.js +2 -0
  15. package/dist/src/cli.js +5 -1
  16. package/dist/src/clickhouse-backing.d.ts +2 -0
  17. package/dist/src/clickhouse-backing.js +145 -0
  18. package/dist/src/command-lifetime.d.mts +1 -1
  19. package/dist/src/command-lifetime.mjs +5 -2
  20. package/dist/src/configs.js +1 -1
  21. package/dist/src/consumers.js +1 -1
  22. package/dist/src/covers.d.ts +4 -2
  23. package/dist/src/covers.js +8 -4
  24. package/dist/src/go-mysql-binding.d.ts +19 -0
  25. package/dist/src/go-mysql-binding.js +51 -0
  26. package/dist/src/guest-processes.d.mts +1 -1
  27. package/dist/src/guest-processes.mjs +22 -14
  28. package/dist/src/host-cli.js +8 -1
  29. package/dist/src/host-ready.d.ts +16 -2
  30. package/dist/src/host-ready.js +66 -39
  31. package/dist/src/host-worker.js +7 -1
  32. package/dist/src/host.d.ts +26 -0
  33. package/dist/src/host.js +93 -3
  34. package/dist/src/infra-definition.d.ts +35 -0
  35. package/dist/src/infra-definition.js +146 -0
  36. package/dist/src/infra.d.ts +3 -0
  37. package/dist/src/infra.js +375 -105
  38. package/dist/src/init.d.ts +46 -0
  39. package/dist/src/init.js +446 -19
  40. package/dist/src/lifecycle-record.d.ts +5 -1
  41. package/dist/src/lifecycle-record.js +27 -4
  42. package/dist/src/local-branches.js +1 -1
  43. package/dist/src/mariadb-bootstrap.d.ts +32 -0
  44. package/dist/src/mariadb-bootstrap.js +90 -0
  45. package/dist/src/mariadb-supplier.d.ts +23 -0
  46. package/dist/src/mariadb-supplier.js +215 -0
  47. package/dist/src/mongodb-backing.d.ts +4 -0
  48. package/dist/src/mongodb-backing.js +71 -1
  49. package/dist/src/native-mariadb-backing.d.ts +13 -0
  50. package/dist/src/native-mariadb-backing.js +512 -0
  51. package/dist/src/native-postgres-backing.d.ts +4 -10
  52. package/dist/src/native-postgres-backing.js +428 -43
  53. package/dist/src/native-server-inputs.d.ts +16 -0
  54. package/dist/src/native-server-inputs.js +204 -0
  55. package/dist/src/origin.js +3 -3
  56. package/dist/src/original-inputs.d.ts +14 -0
  57. package/dist/src/original-inputs.js +84 -0
  58. package/dist/src/pack-facts.d.ts +9 -1
  59. package/dist/src/pglite-backing.d.ts +41 -17
  60. package/dist/src/pglite-backing.js +213 -79
  61. package/dist/src/pglite-host.mjs +482 -10
  62. package/dist/src/process-groups.d.mts +6 -1
  63. package/dist/src/process-groups.d.ts +2 -1
  64. package/dist/src/process-groups.js +1 -1
  65. package/dist/src/process-groups.mjs +82 -15
  66. package/dist/src/process-signal.d.mts +2 -2
  67. package/dist/src/process-signal.mjs +29 -9
  68. package/dist/src/project-inspect.d.ts +12 -0
  69. package/dist/src/project-inspect.js +41 -4
  70. package/dist/src/proxy-env.d.ts +34 -0
  71. package/dist/src/proxy-env.js +114 -0
  72. package/dist/src/redirect-proxy.d.ts +1 -31
  73. package/dist/src/redirect-proxy.js +3 -112
  74. package/dist/src/redis-backing.d.ts +6 -4
  75. package/dist/src/redis-backing.js +20 -168
  76. package/dist/src/runtime.d.ts +1 -0
  77. package/dist/src/runtime.js +503 -218
  78. package/dist/src/schema.d.ts +1 -0
  79. package/dist/src/schema.js +5 -3
  80. package/dist/src/served-world.js +3 -3
  81. package/dist/src/twin-backing.d.ts +47 -0
  82. package/dist/src/twin-backing.js +343 -0
  83. package/dist/src/up-cli.js +56 -3
  84. package/package.json +3 -3
  85. package/src/ancestry.ts +2 -2
  86. package/src/app-url.ts +9 -2
  87. package/src/branch.ts +26 -8
  88. package/src/browser-files.ts +15 -0
  89. package/src/browsers.ts +4 -4
  90. package/src/catalog.ts +9 -4
  91. package/src/changeset.ts +1 -1
  92. package/src/cli-commands.ts +85 -18
  93. package/src/cli-load-clock.ts +2 -0
  94. package/src/cli.ts +5 -1
  95. package/src/clickhouse-backing.ts +70 -0
  96. package/src/command-lifetime.d.mts +1 -1
  97. package/src/command-lifetime.mjs +5 -2
  98. package/src/configs.ts +1 -1
  99. package/src/consumers.ts +1 -1
  100. package/src/covers.ts +9 -4
  101. package/src/go-mysql-binding.ts +49 -0
  102. package/src/guest-processes.d.mts +1 -1
  103. package/src/guest-processes.mjs +22 -14
  104. package/src/host-cli.ts +7 -1
  105. package/src/host-ready.ts +62 -31
  106. package/src/host-worker.ts +6 -2
  107. package/src/host.ts +88 -6
  108. package/src/infra-definition.ts +135 -0
  109. package/src/infra.ts +250 -45
  110. package/src/init.ts +417 -18
  111. package/src/lifecycle-record.ts +18 -4
  112. package/src/local-branches.ts +1 -1
  113. package/src/mariadb-bootstrap.ts +107 -0
  114. package/src/mariadb-supplier.ts +199 -0
  115. package/src/mongodb-backing.ts +59 -1
  116. package/src/native-mariadb-backing.ts +418 -0
  117. package/src/native-postgres-backing.ts +353 -41
  118. package/src/native-server-inputs.ts +169 -0
  119. package/src/origin.ts +3 -3
  120. package/src/original-inputs.ts +72 -0
  121. package/src/pack-facts.ts +1 -1
  122. package/src/pglite-backing.ts +192 -80
  123. package/src/pglite-host.mjs +482 -10
  124. package/src/process-groups.d.mts +6 -1
  125. package/src/process-groups.mjs +82 -15
  126. package/src/process-groups.ts +2 -1
  127. package/src/process-signal.d.mts +2 -2
  128. package/src/process-signal.mjs +29 -9
  129. package/src/project-inspect.ts +37 -4
  130. package/src/proxy-env.ts +128 -0
  131. package/src/redirect-proxy.ts +3 -125
  132. package/src/redis-backing.ts +19 -136
  133. package/src/runtime.ts +342 -107
  134. package/src/schema.ts +5 -4
  135. package/src/served-world.ts +3 -3
  136. package/src/twin-backing.ts +300 -0
  137. package/src/up-cli.ts +52 -3
@@ -1,3 +1,5 @@
1
+ import { ensureCa, leafCertFor, proxyAddressEnv } from "./proxy-env.js";
2
+ export { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, tearDownCa, leafCertFor, proxyEnvFor } from "./proxy-env.js";
1
3
  import { errorMessage } from "./error-message.js";
2
4
  import { serveHttp } from '@volter/world-core/runtime';
3
5
  import { worldNetworkPolicyFromEnv, worldGrantFromEnv, worldStrictEgress, worldNetworkNeedsPathInspection, worldNetworkUsesPassthrough } from '@volter/world-core/network-policy';
@@ -22,8 +24,6 @@ import { websocketRelay } from "./websocket-relay.js";
22
24
  //
23
25
  // Cert tooling: we shell out to `openssl` (already a dev prerequisite; node:crypto can generate keys
24
26
  // but cannot mint X.509 certs without a third-party lib). This keeps world-runtime dependency-light.
25
- import { spawnSync } from 'node:child_process';
26
- import { createHash } from 'node:crypto';
27
27
  import { lookup as dnsLookup } from 'node:dns/promises';
28
28
  import { BlockList, isIP, connect as netConnect, createServer as createNetServer } from 'node:net';
29
29
  import { networkInterfaces } from 'node:os';
@@ -31,8 +31,7 @@ import { connect as tlsConnect } from 'node:tls';
31
31
  import { request as httpsRequest } from 'node:https';
32
32
  import { createServer as createHttpServer } from 'node:http';
33
33
  import { Readable } from 'node:stream';
34
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
35
- import { join } from 'node:path';
34
+ import { readFileSync } from 'node:fs';
36
35
  // The host→twin table is loaded through the shared inject-map module (one world-runtime home
37
36
  // for the injector's data), so this proxy, `covers`, and `up`'s inert-injectEnv warning can
38
37
  // never re-encode the vendor host predicates — they stay one source of truth with the Node
@@ -77,10 +76,6 @@ export function proxyTargetFor(hostname, env, pathname) {
77
76
  const inject = loadInject();
78
77
  return inject.resolveTwin(hostname, inject.readMap(env), pathname);
79
78
  }
80
- /** The vendor→twin map this world actively redirects (for diagnostics / activate output). */
81
- export function activeVendorMap(env) {
82
- return loadInject().readMap(env);
83
- }
84
79
  /** A connection from anything but the proxy host's own loopback is a guest's (a World Machine: its forwarder connects
85
80
  * from 127.0.0.2 on this host, or from its VM's address): architecture, "The scoped proxy's local pass-through". */
86
81
  export function isGuestSource(address) {
@@ -197,91 +192,6 @@ function guestPinRefusal(target) {
197
192
  return { outcome: 'refuse', capability: 'network.egress', target, missing: 'transport', rule: 'guest-inspected',
198
193
  reason: 'inspected egress cannot keep a guest\'s checked destination pin' };
199
194
  }
200
- function caPaths(tlsDir) {
201
- return { dir: tlsDir, caKey: join(tlsDir, 'ca-key.pem'), caCert: join(tlsDir, 'ca-cert.pem') };
202
- }
203
- /** Whether this host opens raw sockets to other hosts, which the ambient TLS redirect proxy exists to intercept. A
204
- * browser tab's Node (tabnode, which declares `process.versions.webcontainer`) opens none: its TLS is loopback pairing
205
- * with no handshake, and a remote HTTPS call leaves through the page's HTTP transport. There no proxy can run and none
206
- * is needed: the World's network policy is enforced by the injector in every Node program (world-core inject.cjs). */
207
- export function hostHasRawEgress(versions = process.versions) {
208
- return versions.webcontainer === undefined;
209
- }
210
- export function opensslAvailable() {
211
- // TEST-ONLY seam (TWIN-64): forces "openssl unavailable" deterministically. A PATH-shim (fake
212
- // `openssl` on PATH) doesn't reliably work here because `spawnSync('openssl', …)` below is called
213
- // with no explicit `env`, and some runtimes resolve the executable against a PATH snapshotted at
214
- // process start rather than a same-process `process.env.PATH` mutation made mid-test. Never set
215
- // this env var in real usage.
216
- if (process.env.VOLTER_TEST_NO_OPENSSL === '1')
217
- return false;
218
- const r = spawnSync('openssl', ['version'], { windowsHide: true, stdio: 'ignore' });
219
- return r.status === 0;
220
- }
221
- function runOpenssl(args) {
222
- const r = spawnSync('openssl', args, { windowsHide: true, encoding: 'utf8' });
223
- if (r.status !== 0)
224
- throw new Error(`openssl ${args[0]} failed: ${(r.stderr || r.stdout || '').trim()}`);
225
- return r.stdout;
226
- }
227
- /** Generate (once) a throwaway CA in `tlsDir`. Idempotent: reuses an existing CA so a re-activate in
228
- * the same world trusts the same cert. Session/project-scoped — it lives under the instance dir and
229
- * is removed by `tearDownCa` on `down`. */
230
- export function ensureCa(tlsDir) {
231
- if (!opensslAvailable()) {
232
- throw new Error('openssl not found — the TLS redirect proxy needs openssl to mint a session CA. Install openssl, or use per-CLI endpoint env (cliRedirect) instead.');
233
- }
234
- const paths = caPaths(tlsDir);
235
- if (existsSync(paths.caKey) && existsSync(paths.caCert))
236
- return paths;
237
- mkdirSync(tlsDir, { recursive: true });
238
- runOpenssl(['genrsa', '-out', paths.caKey, '2048']);
239
- runOpenssl([
240
- 'req', '-x509', '-new', '-nodes', '-key', paths.caKey,
241
- '-sha256', '-days', '3650', '-out', paths.caCert,
242
- '-subj', '/CN=volter-world session CA/O=volter-world',
243
- // A CA cert without keyUsage is refused by OpenSSL 3 (Python, curl on Debian 13: "CA cert does not
244
- // include key usage extension"), so the CA states what a CA is for.
245
- '-addext', 'basicConstraints=critical,CA:TRUE',
246
- '-addext', 'keyUsage=critical,keyCertSign,cRLSign',
247
- '-addext', 'subjectKeyIdentifier=hash',
248
- ]);
249
- return paths;
250
- }
251
- /** Remove the session CA + any minted leaf certs. Called on `down` so the trusted CA never outlives
252
- * the world (it was only ever trusted via the per-shell env, never system-wide). */
253
- export function tearDownCa(tlsDir) {
254
- leafCache.clear();
255
- rmSync(tlsDir, { recursive: true, force: true });
256
- }
257
- const leafCache = new Map();
258
- /** Mint (and cache) a leaf cert for `host`, signed by the session CA. SAN = the host so the client
259
- * accepts it for that SNI. */
260
- export function leafCertFor(host, ca) {
261
- // A pathname can hold a replacement CA; its certificate identifies the signing generation.
262
- const cacheKey = `${createHash('sha256').update(readFileSync(ca.caCert)).digest('hex')}:${host}`;
263
- const cached = leafCache.get(cacheKey);
264
- if (cached)
265
- return cached;
266
- const safe = host.replace(/[^A-Za-z0-9._-]/g, '_');
267
- const keyPath = join(ca.dir, `leaf-${safe}-key.pem`);
268
- const csrPath = join(ca.dir, `leaf-${safe}.csr`);
269
- const certPath = join(ca.dir, `leaf-${safe}.pem`);
270
- const extPath = join(ca.dir, `leaf-${safe}.ext`);
271
- runOpenssl(['genrsa', '-out', keyPath, '2048']);
272
- runOpenssl(['req', '-new', '-key', keyPath, '-out', csrPath, '-subj', `/CN=${host}`]);
273
- writeFileSync(extPath, `subjectAltName=DNS:${host}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`);
274
- runOpenssl([
275
- // 397 days, NOT the CA's 3650: macOS's system verifier (used by Go — `gh`, and any Go/Swift
276
- // client) rejects a LEAF cert valid >398 days as "certificate is not standards compliant"
277
- // (CA/Browser-Forum max). The session CA itself is exempt and stays long-lived.
278
- 'x509', '-req', '-in', csrPath, '-CA', ca.caCert, '-CAkey', ca.caKey,
279
- '-CAcreateserial', '-out', certPath, '-days', '397', '-sha256', '-extfile', extPath,
280
- ]);
281
- const minted = { key: readFileSync(keyPath, 'utf8'), cert: readFileSync(certPath, 'utf8') };
282
- leafCache.set(cacheKey, minted);
283
- return minted;
284
- }
285
195
  /** A failed twin request's answer: the twin, its origin, the World and the cause, so the app's error says which service
286
196
  * of which World to look at (`doctor`). Shared by every front that relays to twins. */
287
197
  export function twinUnreachableMessage(vendor, origin, err, world) {
@@ -853,22 +763,3 @@ export async function startRedirectProxy(options) {
853
763
  }),
854
764
  };
855
765
  }
856
- /** The cert-trust + proxy env that points an unmodified tool through the proxy and trusts the session
857
- * CA — and ONLY this session (these are per-shell exports; nothing is installed system-wide). Mirrors
858
- * the env list in WORLD_ACTIVATE.md §"The honest cost: a scoped CA". */
859
- export function proxyEnvFor(proxyUrl, caCertPath, caller = process.env, guestUrl) {
860
- return { ...proxyAddressEnv(proxyUrl, caCertPath, guestUrl), ...sessionTrustEnv(caCertPath, caller) };
861
- }
862
- /** The proxy's address env; `VOLTER_WORLD_GUEST_PROXY` is its guests' listener, which a World Machine's forwarder
863
- * targets (architecture, "The scoped proxy's local pass-through"). */
864
- function proxyAddressEnv(proxyUrl, caCertPath, guestUrl) {
865
- return {
866
- ...(guestUrl ? { VOLTER_WORLD_GUEST_PROXY: guestUrl } : {}),
867
- HTTPS_PROXY: proxyUrl,
868
- HTTP_PROXY: proxyUrl,
869
- https_proxy: proxyUrl,
870
- http_proxy: proxyUrl,
871
- VOLTER_WORLD_PROXY: proxyUrl,
872
- VOLTER_WORLD_CA: caCertPath,
873
- };
874
- }
@@ -1,8 +1,10 @@
1
+ import { type ManagedProtocol } from './twin-backing.js';
1
2
  import type { HostLaunch, InfraService } from './pglite-backing.js';
3
+ export declare const REDIS_PROTOCOL: ManagedProtocol;
2
4
  /** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
3
- export declare function redisTwinCli(from: string[]): string | undefined;
5
+ export declare const redisTwinCli: (from: string[]) => string | undefined;
4
6
  /** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
5
7
  export declare const redisTwinRoot: (dataDir: string) => string;
6
- export declare function redisTwinUp(services: InfraService[], dataDir: string, cli: string, launch?: HostLaunch): Promise<void>;
7
- export declare function redisTwinStatus(services: InfraService[], dataDir: string): Promise<number>;
8
- export declare function redisTwinDown(services: InfraService[], dataDir: string): Promise<void>;
8
+ export declare const redisTwinUp: (services: InfraService[], dataDir: string, cli: string, launch?: HostLaunch) => Promise<void>;
9
+ export declare const redisTwinStatus: (services: InfraService[], dataDir: string) => Promise<number>;
10
+ export declare const redisTwinDown: (services: InfraService[], dataDir: string) => Promise<void>;
@@ -8,43 +8,13 @@
8
8
  // in a container is state outside it (a branch of the World would share the container's queue and cache). The
9
9
  // twin keeps each key a subject of the World (the Redis pack's storage module). It needs nothing
10
10
  // installed: no container runtime, no redis-server binary.
11
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync } from 'node:fs';
11
+ //
12
+ // The lifecycle (start, ownership, readiness, retirement) is every twin-served service's and lives in
13
+ // twin-backing.ts, and the pack that serves the kind is found there from pack facts (`managedService`), not named
14
+ // here. This file is what is Redis's own in the runtime: the two questions asked in Redis's protocol.
12
15
  import { connect } from 'node:net';
13
- import { dirname, join } from 'node:path';
14
- import { commandLifetime, COMMAND_GRACE_MS, COMMAND_CONFIRM_MS } from "./command-lifetime.js";
15
- import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, signalOwnedProcess } from "./process-receipt.js";
16
- import { spawn } from 'node:child_process';
17
- import { survivingOwnedGroups } from "./process-groups.js";
18
- import { errorMessage } from "./error-message.js";
19
- import { findInstalledPackage, packCli } from "./catalog.js";
20
- import { vendorPackageDir } from '@volter/world-core/runtime';
21
- const hosts = new Map();
22
- const PACKAGE = '@volter/twin-redis';
23
- const READY_TIMEOUT_MS = 60_000;
24
- /** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
25
- export function redisTwinCli(from) {
26
- for (const dir of from) {
27
- const found = vendorPackageDir(dir, 'redis') ?? findInstalledPackage(dir, PACKAGE);
28
- const cli = found === undefined ? undefined : packCli(found);
29
- if (cli !== undefined && existsSync(cli))
30
- return cli;
31
- }
32
- return undefined;
33
- }
34
- const pidPath = (dataDir, service) => join(dataDir, `redis-twin-${service.hostPort}.pid`);
35
- /** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
36
- export const redisTwinRoot = (dataDir) => join(dataDir, 'redis-twin');
37
- function alive(pid) {
38
- try {
39
- process.kill(pid, 0);
40
- return true;
41
- }
42
- catch (error) {
43
- if (error.code === 'ESRCH')
44
- return false;
45
- throw error;
46
- }
47
- }
16
+ import { join } from 'node:path';
17
+ import { managedDirectory, managedTwin, twinBackingCli, twinBackingDown, twinBackingStatus, twinBackingUp } from "./twin-backing.js";
48
18
  function listening(port) {
49
19
  return new Promise((resolve) => {
50
20
  const socket = connect({ host: '127.0.0.1', port, timeout: 1000 });
@@ -82,135 +52,17 @@ function redisServes(port, pid) {
82
52
  socket.on('timeout', () => { socket.destroy(); resolve(false); });
83
53
  });
84
54
  }
85
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
86
- export async function redisTwinUp(services, dataDir, cli, launch = {}) {
87
- for (const service of services) {
88
- const pidFile = pidPath(dataDir, service);
89
- if (hosts.has(pidFile) || existsSync(pidFile) || existsSync(spawnReceiptPath(pidFile))) {
90
- await redisTwinDown([service], dataDir);
91
- }
92
- // a port that already answers is another process's (another World's twin, a redis-server): serving there would
93
- // hand this World's app someone else's keys, so it is refused, never taken as ready
94
- if (await listening(service.hostPort))
95
- throw new Error(`port ${service.hostPort} already serves Redis for another process`);
96
- const root = redisTwinRoot(dataDir);
97
- mkdirSync(root, { recursive: true });
98
- const log = join(dataDir, `redis-twin-${service.hostPort}.log`);
99
- mkdirSync(dirname(log), { recursive: true });
100
- // a real fd, not a pipe: the server outlives this process untethered
101
- const logFd = openSync(log, 'a');
102
- // the runtime this runs on: Bun runs the pack's TypeScript as it is; Node (volter-world-infra's shebang) needs
103
- // type transformation, not only stripping (the kernel declares constructor parameter properties)
104
- const runtime = typeof globalThis.Bun !== 'undefined' || !cli.endsWith('.ts') ? [] : ['--experimental-transform-types', '--no-warnings'];
105
- let held;
106
- try {
107
- try {
108
- const child = spawn(process.execPath, [...runtime, cli, 'serve', '--port', String(service.hostPort), '--root', root], {
109
- env: launch.env ?? process.env, detached: true, windowsHide: true, stdio: ['ignore', logFd, logFd],
110
- });
111
- // Retain BEFORE descriptor close, birth capture or either publication can fail.
112
- held = { child, lifetime: commandLifetime(child, launch.signal), closed: false };
113
- const observed = held;
114
- child.once('close', () => { observed.closed = true; });
115
- child.on('error', () => { }); // lifetime joins spawn failure and close
116
- hosts.set(pidFile, held);
117
- }
118
- finally {
119
- closeSync(logFd);
120
- }
121
- if (!held.child.pid)
122
- throw new Error('Redis child did not start');
123
- const identity = await held.lifetime.identity;
124
- if (!identity?.start)
125
- throw new Error('Redis birth capture failed; retiring held child');
126
- publishProcessReceipt(spawnReceiptPath(pidFile), JSON.stringify(identity) + '\n');
127
- publishProcessReceipt(pidFile, `${held.child.pid}\n`);
128
- const deadline = Date.now() + READY_TIMEOUT_MS;
129
- while (!alive(held.child.pid) || !(await redisServes(service.hostPort, held.child.pid))) {
130
- if (!alive(held.child.pid))
131
- throw new Error(`redis exited before serving — see ${log}`);
132
- launch.signal?.throwIfAborted();
133
- if (Date.now() > deadline)
134
- throw new Error(`redis did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
135
- await sleep(25);
136
- }
137
- launch.signal?.throwIfAborted();
138
- held.child.unref();
139
- }
140
- catch (error) {
141
- if (held) {
142
- try {
143
- await redisTwinDown([service], dataDir);
144
- }
145
- catch (cleanup) {
146
- throw new AggregateError([error, cleanup], 'Redis startup and retirement failed; ownership retained');
147
- }
148
- }
149
- throw error;
150
- }
151
- }
152
- }
153
- export async function redisTwinStatus(services, dataDir) {
154
- let ready = 0;
155
- for (const service of services) {
156
- const pidFile = pidPath(dataDir, service);
157
- if (!existsSync(pidFile))
158
- continue;
159
- const pid = Number(readFileSync(pidFile, 'utf8').trim());
160
- if (alive(pid) && await redisServes(service.hostPort, pid))
161
- ready += 1;
162
- }
163
- return ready;
164
- }
165
- export async function redisTwinDown(services, dataDir) {
166
- for (const service of services) {
167
- const pidFile = pidPath(dataDir, service);
168
- const recorded = readProcessReceipt(pidFile, text => {
169
- const pid = Number(text.trim());
170
- if (!Number.isInteger(pid) || pid <= 0)
171
- throw new Error('Invalid Redis ownership PID');
172
- return pid;
173
- });
174
- const identity = readSpawnReceipt(pidFile);
175
- const held = hosts.get(pidFile);
176
- if (recorded.error || identity.error)
177
- process.stderr.write(`${errorMessage(new AggregateError([recorded.error, identity.error].filter(Boolean), 'Redis ownership uncertain; retrying established ownership'))}\n`);
178
- if (held) {
179
- const retired = () => held.closed && (!held.child.pid || !survivingOwnedGroups([held.child.pid]).length);
180
- if (!retired()) {
181
- held.child.ref();
182
- held.lifetime.cancel('SIGTERM');
183
- try {
184
- await held.lifetime.finish();
185
- }
186
- catch (error) {
187
- if (!retired())
188
- throw error;
189
- }
190
- }
191
- }
192
- else {
193
- if (!recorded.present && !identity.present)
194
- continue;
195
- const pid = identity.value?.pid ?? recorded.value;
196
- if (!pid)
197
- throw new AggregateError([recorded.error, identity.error].filter(Boolean), 'Redis ownership uncertain; receipts retained');
198
- if (recorded.value && identity.value && recorded.value !== identity.value.pid)
199
- throw new Error('Redis ownership receipts disagree; retirement refused; receipts retained');
200
- const live = () => survivingOwnedGroups([pid]).length > 0;
201
- for (const [signal, limit] of [['SIGTERM', COMMAND_GRACE_MS], ['SIGKILL', COMMAND_CONFIRM_MS]]) {
202
- if (!live())
203
- break;
204
- await signalOwnedProcess(identity.value, signal, true);
205
- const deadline = Date.now() + limit;
206
- while (live() && Date.now() < deadline)
207
- await sleep(20);
208
- }
209
- if (live())
210
- throw new Error('Redis retirement unconfirmed; receipts retained');
211
- }
212
- rmSync(pidFile, { force: true });
213
- rmSync(spawnReceiptPath(pidFile), { force: true });
214
- hosts.delete(pidFile);
215
- }
216
- }
55
+ export const REDIS_PROTOCOL = { name: 'Redis', answers: listening, serves: redisServes };
56
+ const twin = () => {
57
+ const found = managedTwin('redis', REDIS_PROTOCOL);
58
+ if (!found)
59
+ throw new Error('no pack declares the redis kind (managedService) in pack facts');
60
+ return found;
61
+ };
62
+ /** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
63
+ export const redisTwinCli = (from) => { const found = managedTwin('redis', REDIS_PROTOCOL); return found && twinBackingCli(found, from); };
64
+ /** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
65
+ export const redisTwinRoot = (dataDir) => join(dataDir, managedDirectory('redis'));
66
+ export const redisTwinUp = (services, dataDir, cli, launch = {}) => twinBackingUp(twin(), services, dataDir, cli, launch);
67
+ export const redisTwinStatus = (services, dataDir) => twinBackingStatus(twin(), services, dataDir);
68
+ export const redisTwinDown = (services, dataDir) => twinBackingDown(twin(), services, dataDir);
@@ -255,6 +255,7 @@ export declare function attachWorld(name: string, command: string[], root?: stri
255
255
  cwd?: string;
256
256
  owner?: string;
257
257
  verbose?: boolean;
258
+ instance?: WorldInstance;
258
259
  }): Promise<number>;
259
260
  /** The shell + env + cwd a `volter-world shell` subshell launches with (extracted for testability):
260
261
  * the world env (twin `*_URL`s + fake keys + the Node injector + cliRedirect endpoint vars) on top