@volter/world-runtime 3.0.162 → 3.0.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) 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 +3 -2
  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 +445 -18
  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 +15 -10
  81. package/dist/src/service-exit.d.ts +3 -2
  82. package/dist/src/service-exit.js +7 -5
  83. package/dist/src/twin-backing.d.ts +47 -0
  84. package/dist/src/twin-backing.js +343 -0
  85. package/dist/src/up-cli.js +56 -3
  86. package/package.json +4 -4
  87. package/src/ancestry.ts +2 -2
  88. package/src/app-url.ts +9 -2
  89. package/src/branch.ts +26 -8
  90. package/src/browser-files.ts +15 -0
  91. package/src/browsers.ts +4 -4
  92. package/src/catalog.ts +4 -3
  93. package/src/changeset.ts +1 -1
  94. package/src/cli-commands.ts +85 -18
  95. package/src/cli-load-clock.ts +2 -0
  96. package/src/cli.ts +5 -1
  97. package/src/clickhouse-backing.ts +70 -0
  98. package/src/command-lifetime.d.mts +1 -1
  99. package/src/command-lifetime.mjs +5 -2
  100. package/src/configs.ts +1 -1
  101. package/src/consumers.ts +1 -1
  102. package/src/covers.ts +9 -4
  103. package/src/go-mysql-binding.ts +49 -0
  104. package/src/guest-processes.d.mts +1 -1
  105. package/src/guest-processes.mjs +22 -14
  106. package/src/host-cli.ts +7 -1
  107. package/src/host-ready.ts +62 -31
  108. package/src/host-worker.ts +6 -2
  109. package/src/host.ts +88 -6
  110. package/src/infra-definition.ts +135 -0
  111. package/src/infra.ts +250 -45
  112. package/src/init.ts +416 -17
  113. package/src/lifecycle-record.ts +18 -4
  114. package/src/local-branches.ts +1 -1
  115. package/src/mariadb-bootstrap.ts +107 -0
  116. package/src/mariadb-supplier.ts +199 -0
  117. package/src/mongodb-backing.ts +59 -1
  118. package/src/native-mariadb-backing.ts +418 -0
  119. package/src/native-postgres-backing.ts +353 -41
  120. package/src/native-server-inputs.ts +169 -0
  121. package/src/origin.ts +3 -3
  122. package/src/original-inputs.ts +72 -0
  123. package/src/pack-facts.ts +1 -1
  124. package/src/pglite-backing.ts +192 -80
  125. package/src/pglite-host.mjs +482 -10
  126. package/src/process-groups.d.mts +6 -1
  127. package/src/process-groups.mjs +82 -15
  128. package/src/process-groups.ts +2 -1
  129. package/src/process-signal.d.mts +2 -2
  130. package/src/process-signal.mjs +29 -9
  131. package/src/project-inspect.ts +37 -4
  132. package/src/proxy-env.ts +128 -0
  133. package/src/redirect-proxy.ts +3 -125
  134. package/src/redis-backing.ts +19 -136
  135. package/src/runtime.ts +342 -107
  136. package/src/schema.ts +5 -4
  137. package/src/served-world.ts +14 -10
  138. package/src/service-exit.ts +5 -3
  139. package/src/twin-backing.ts +300 -0
  140. package/src/up-cli.ts +52 -3
package/src/infra.ts CHANGED
@@ -7,13 +7,15 @@
7
7
  // that owns the World's lifetime, as every other service's recorder is, and not of a tool that exits once they
8
8
  // serve. A process layer where a child does not outlive its parent (a browser tab's) otherwise ended the PGlite host
9
9
  // the moment `volter-world-infra up` returned, and the World's readiness probe never saw it.
10
- import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
10
+ import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
11
11
  import { delimiter, dirname, join } from 'node:path';
12
+ import { setWorldEventsFile, worldEvent } from './process-groups.ts';
12
13
  import { signalChild } from './process-signal.mjs';
13
14
  import { spawn } from 'node:child_process';
14
15
  import { fileURLToPath } from 'node:url';
15
16
  import { containerlessDataDirectory, infraConnections, liveDatabaseBehind, liveDatabaseMigration, developmentMigratesAtRuntime, DEVELOPMENT_MIGRATION_NOTICE, parseInfraDefinition } from './pglite-backing.ts';
16
17
  import { errorMessage } from './error-message.ts';
18
+ import type { ManagedProtocol } from './twin-backing.ts';
17
19
 
18
20
  /** Whether a program is on PATH, read from the file system without starting a process: where a missing program
19
21
  * still costs a process round trip (a browser tab's kernel), the container probe is not spawned at all. */
@@ -24,7 +26,13 @@ function onPath(name: string, env: NodeJS.ProcessEnv): boolean {
24
26
  });
25
27
  }
26
28
 
27
- export interface InfrastructureIo { out: (text: string) => void; err: (text: string) => void }
29
+ export interface InfrastructureIo {
30
+ out: (text: string) => void;
31
+ err: (text: string) => void;
32
+ /** The runtime's own in-process `up` (runtime.ts): handed the status its `status` would print, when this `up` itself
33
+ * proved everything that status proves (every service twin-served, each held and serving). */
34
+ verified?: (status: string) => void;
35
+ }
28
36
 
29
37
  interface Ran { status: number | null; stdout: string; stderr: string; error?: Error }
30
38
 
@@ -122,11 +130,39 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
122
130
  io.err('managed infrastructure: missing World lifecycle context\n');
123
131
  return 2;
124
132
  }
133
+ // What this command does to a backing's process is said in the World's own log, beside its services' events.
134
+ setWorldEventsFile(join(dirname(worldData), 'logs', 'events.jsonl'));
135
+ const phaseAsked = Date.now();
136
+ // Retirement follows retained ownership even if the definition or supplier
137
+ // can no longer be read. Other infrastructure still reports its own refusal.
138
+ let retainedMariaDB = false;
139
+ if (phase === 'down') {
140
+ try {
141
+ retainedMariaDB = existsSync(join(worldData, 'native-mariadb.owner.json'))
142
+ || existsSync(join(worldData, 'native-mariadb.owner.json.spawn.json'))
143
+ || existsSync(join(worldData, 'native-mariadb.starting.json'))
144
+ // install-db can outlive its caller before any server receipt exists.
145
+ // Either half of a helper pair enters retirement; a missing birth
146
+ // token remains an explicit uncertainty in nativeMariaDBDown.
147
+ || (existsSync(worldData) && readdirSync(worldData).some(name => /^native-mariadb-tool-.*\.json(?:\.spawn\.json)?$/u.test(name)));
148
+ } catch (error) { io.err(`managed MariaDB ownership inventory is unreadable: ${errorMessage(error)}\n`); return 1; }
149
+ }
150
+ if (retainedMariaDB) {
151
+ try { await (await import('./native-mariadb-backing.ts')).nativeMariaDBDown(worldData); }
152
+ catch (error) { io.err(`managed MariaDB down failed: ${errorMessage(error)}\n`); return 1; }
153
+ }
125
154
  const definition = join(dirname(worldConfig), 'world.infrastructure.yml');
126
155
  if (!existsSync(definition)) {
127
156
  io.err('managed infrastructure: declared definition is missing\n');
128
157
  return 2;
129
158
  }
159
+ const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
160
+ // Both the CLI and the World's in-process up enter here. Endpoint metadata cannot supply host DNS or a native
161
+ // default-port listener: refuse before choosing or starting any backing when the substrate carrier is absent.
162
+ if (phase === 'up' && declared.some(service => service.virtualTcpEndpoint) && env.VOLTER_WORLD_VIRTUAL_TCP !== '1') {
163
+ io.err('managed infrastructure up: virtual TCP endpoints require the substrate network carrier; native host DNS/default ports are not supplied\n');
164
+ return 2;
165
+ }
130
166
  // A development World says so every time it comes up: its use is never silent.
131
167
  if (phase === 'up' && developmentMigratesAtRuntime(worldConfig)) io.out(`${DEVELOPMENT_MIGRATION_NOTICE}\n`);
132
168
 
@@ -218,7 +254,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
218
254
  const { unsupportedKinds } = await import('./pglite-backing.ts');
219
255
  const { nativePostgresTools } = await import('./native-postgres-backing.ts');
220
256
  const postgres = declared.filter(s => s.kind === 'postgres');
221
- if (postgres.length !== 1 || unsupportedKinds(declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s))).length) return selected;
257
+ if (postgres.length !== 1 || unsupportedKinds(declared.filter(s => s.kind !== 'postgres' && !s.nativeMariaDB && !twinned.includes(s))).length) return selected;
222
258
  const native = existsSync(join(worldData!, 'native-postgres-data'));
223
259
  const pglite = existsSync(join(worldData!, 'pglite-postgres-data'));
224
260
  if (native && pglite) {
@@ -257,7 +293,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
257
293
  io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
258
294
  return 1;
259
295
  }
260
- io.out('managed infrastructure ready\n');
296
+ sayReady();
261
297
  return 0;
262
298
  }
263
299
  if (phase === 'status') {
@@ -275,18 +311,68 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
275
311
  return 0;
276
312
  }
277
313
 
278
- // ---- redis: the twin, containerless ----------------------------------------
279
- // A redis service is served by the redis twin (redis-backing.ts) on every machine, container runtime or not,
280
- // unless the operator forces the container backing; the other services take the backing chosen above. Where the
281
- // twin is not installed (@volter/twin-redis is not this runtime's dependency), redis goes to that backing too:
282
- // the container where there is one, else the backing's refusal naming redis.
283
- const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
284
- const { redisTwinCli } = await import('./redis-backing.ts');
285
- const twinCli = redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)]);
286
- const twinned = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || twinCli === undefined ? [] : declared.filter((s) => s.kind === 'redis');
314
+ // ---- a kind a twin serves: the twin, containerless --------------------------
315
+ // A service whose kind an installed pack declares (`managedService` in pack facts; twin-backing.ts) is served by
316
+ // that pack on every machine, container runtime or not, unless the operator forces the container backing; the
317
+ // other services take the backing chosen above. Which pack is the pack's declaration, never a name here. Where no
318
+ // pack declares the kind, or the one that does is not installed, the kind goes to that backing too: the container
319
+ // where there is one, else the backing's refusal naming the kind (the vendor's own server for MongoDB).
320
+ // The kinds listed are those whose protocol this runtime can ask its two questions in (ManagedProtocol).
321
+ const mariaDB = declared.flatMap(service => service.nativeMariaDB ? [service.nativeMariaDB] : []);
322
+ if (phase !== 'down' && mariaDB.length && ['docker', 'pglite'].includes(env.VOLTER_WORLD_INFRA_BACKING ?? '')) {
323
+ io.err('managed MariaDB: explicit native selection cannot be replaced by the forced backing\n');
324
+ return 1;
325
+ }
326
+ // Resolve only kinds the definition actually asks for. Looking up a pack before filtering its services made a
327
+ // PostgreSQL-only World scan installed Redis/MongoDB packs and their CLI graphs on every infrastructure phase.
328
+ const kinds = (['redis', 'mongodb', 'clickhouse'] as const).filter(kind => declared.some(service => service.kind === kind));
329
+ const managed = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || kinds.length === 0
330
+ ? undefined : await import('./twin-backing.ts');
331
+ const protocols: { redis?: ManagedProtocol; mongodb?: ManagedProtocol; clickhouse?: ManagedProtocol } = {};
332
+ if (managed) {
333
+ if (kinds.includes('redis')) protocols.redis = (await import('./redis-backing.ts')).REDIS_PROTOCOL;
334
+ if (kinds.includes('mongodb')) protocols.mongodb = (await import('./mongodb-backing.ts')).MONGODB_PROTOCOL;
335
+ if (kinds.includes('clickhouse')) protocols.clickhouse = (await import('./clickhouse-backing.ts')).CLICKHOUSE_PROTOCOL;
336
+ }
337
+ const expectedTwinPackages = new Map<string, string>();
338
+ const twins = !managed ? [] : kinds.flatMap((kind) => {
339
+ const services = declared.filter((s) => s.kind === kind);
340
+ const beside = [dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)];
341
+ const twin = managed.managedTwin(kind, protocols[kind]!, beside, phase !== 'down' && services.some(service => service.originalInputs !== undefined));
342
+ if (twin) expectedTwinPackages.set(kind, twin.package);
343
+ const cli = twin && managed.twinBackingCli(twin, beside);
344
+ return twin && (cli || phase === 'down') ? [{ twin, cli: cli ?? '', services }] : [];
345
+ });
346
+ const twinned = twins.flatMap((served) => served.services);
347
+ // A declared original-input carrier cannot fall through to a backing that drops it.
348
+ // Down still retires established owners even when a pack or source snapshot is unavailable.
349
+ if (phase !== 'down') {
350
+ try {
351
+ for (const service of declared) if (service.originalInputs) {
352
+ const consumer = twins.find(entry => entry.services.includes(service));
353
+ if (!managed || !consumer) throw new Error(`managed ${service.kind} has original inputs and no installed declaring consumer`);
354
+ managed.originalInputArguments(consumer.twin, service, worldConfig);
355
+ }
356
+ } catch (error) {
357
+ io.err(`managed infrastructure ${phase} refused original inputs: ${diagnostic(errorMessage(error))}\n`);
358
+ return 1;
359
+ }
360
+ }
287
361
  /** The services the backing below answers for: every one, or those the twin does not serve (by compose service name). */
288
- const native = declared.filter((s) => s.kind === 'mongodb');
289
- const composed = twinned.length === 0 && native.length === 0 ? null : declared.filter((s) => !twinned.includes(s) && !native.includes(s)).map((s) => s.kind);
362
+ const native = declared.filter((s) => s.kind === 'mongodb' && !twinned.includes(s));
363
+ // Tab hosts expose their kernel tree on this symbol (world-store.ts). Redis/MongoDB's managed kinds need
364
+ // their installed declaring twins there; a missing pack is an input refusal, never a native-server attempt.
365
+ // PostgreSQL is separate: its available WALI server tools or PGlite still own that declared backing.
366
+ const inTab = (process as unknown as Record<symbol, unknown>)[Symbol.for('tabnode.run.vfs')] !== undefined;
367
+ if (inTab && phase !== 'down') {
368
+ const missing = kinds.filter(kind => !twinned.some(service => service.kind === kind)
369
+ && (env.VOLTER_WORLD_INFRA_BACKING !== 'docker' || native.some(service => service.kind === kind)));
370
+ if (missing.length) {
371
+ for (const kind of missing) io.err(`managed ${kind} needs ${expectedTwinPackages.get(kind) ?? `@volter/twin-${kind}`} installed in this tab World\n`);
372
+ return 1;
373
+ }
374
+ }
375
+ const composed = twinned.length === 0 && native.length === 0 && mariaDB.length === 0 ? null : declared.filter((s) => !twinned.includes(s) && !native.includes(s) && !s.nativeMariaDB).map((s) => s.kind);
290
376
  // A database's native wire and storage are its vendor server's. MongoDB never needs a Docker probe or container.
291
377
  if (native.length > 0) {
292
378
  const { pgliteUp, pgliteStatus, pgliteDown } = await import('./pglite-backing.ts');
@@ -305,65 +391,146 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
305
391
  return 1;
306
392
  }
307
393
  }
308
- if (twinned.length > 0) {
309
- const { redisTwinDown, redisTwinStatus, redisTwinUp } = await import('./redis-backing.ts');
310
- const cli = twinCli!;
311
- if (phase === 'up') {
312
- io.out('managed infrastructure backing: redis twin (containerless)\n');
313
- try {
314
- await redisTwinUp(twinned, worldData!, cli, launch);
315
- } catch (error) {
316
- io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
317
- return 1;
318
- }
319
- } else if (phase === 'status') {
320
- const ready = await redisTwinStatus(twinned, worldData!);
321
- if (ready !== twinned.length) {
322
- io.err(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
323
- return 1;
394
+ // THE TWIN-SERVED KINDS WAIT FOR THE DATABASE'S CLUSTER PLACEMENT. A store staged with a prepared cluster is placed where its
395
+ // twin serves from, and only beside a database that carries the same migration (takePreparedStore). Brought up
396
+ // first, a twin's store was placed before the database's backing was chosen, and where that backing then
397
+ // declined the prepared cluster (its programs' stamp, PGlite serving a native cluster) the World had an
398
+ // unprepared database and a prepared store. On `up` with the native backing, its onPlaced starts the twins once
399
+ // the cluster is placed, beside readiness; otherwise they start after the database. The live cluster is then
400
+ // there to be read; `status` and `down` keep their order. The ready line is said once,
401
+ // after both.
402
+ /** What `status` prints when every declared service is twin-served: one fact, said by status and handed over by up. */
403
+ const twinServedStatus = () => ({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() });
404
+ /** The native backing's status, its proof asked as status asks it: set by the native branch's `up` once it is ready. */
405
+ let nativeProof: (() => Promise<string | undefined>) | undefined;
406
+ /** What this `up` has proved, handed to the runtime's own in-process up (InfrastructureIo.verified) as the status its
407
+ * `status` would print, its proofs asked again now: every twin held (twinBackingHeld), and then either every service
408
+ * twin-served or, with no MongoDB native, the native backing's own status proof (nativeStatus: the containerless
409
+ * others serving, pgliteStatus, and the postmaster's, nativePostgresStatus). It does not repeat status's choice of
410
+ * backing (selectBacking, a `docker info` probe): the backing is the one this up just brought up. Any doubt hands
411
+ * nothing over, and the runtime's probe runs. */
412
+ const handOver = async (): Promise<void> => {
413
+ if (!io.verified || phase !== 'up' || native.length !== 0) return;
414
+ for (const { twin, services } of twins) if (!await managed!.twinBackingHeld(twin, services, worldData!)) return;
415
+ if (mariaDB.length && !await (await import('./native-mariadb-backing.ts')).nativeMariaDBStatus(mariaDB[0]!, worldData, worldConfig, launch)) return;
416
+ if (composed?.length === 0) { io.verified(`${JSON.stringify(twinServedStatus())}\n`); return; }
417
+ if (!nativeProof) return;
418
+ let text: string | undefined;
419
+ try { text = await nativeProof(); } catch { return; }
420
+ if (text) io.verified(text);
421
+ };
422
+ let twinsPending = false;
423
+ let readyHeld = false;
424
+ // THE TWINS COME UP BESIDE THE DATABASE'S READINESS (S2 R79), not after it: Twenty's redis twin began only when the
425
+ // postmaster answered (+1.36 s) and held up 330 ms more. 840a1ae3's rule stands: a twin's staged store is placed beside
426
+ // the cluster the backing chose, so the twins start no earlier than the native backing's onPlaced, said after the
427
+ // cluster is placed (or made by initdb) and its server spawned. Every other backing still runs them after it returns.
428
+ let twinsBeside: Promise<number> | undefined;
429
+ // A twin that fails beside the readiness wait ends that wait (its signal), so the up fails then, not at the
430
+ // database's readiness or deadline: the native start retires its postmaster as on any abort, and the failed-database
431
+ // path below brings the rest down.
432
+ const twinFailed = new AbortController();
433
+ const startTwinsBeside = (): void => {
434
+ if (!twinsPending || twinsBeside) return;
435
+ io.out('managed infrastructure: twins started beside the database\'s readiness (its cluster placed)\n');
436
+ twinsBeside = twinsPhase().then((code) => {
437
+ if (code !== 0) twinFailed.abort(new Error('a twin failed beside the database\'s readiness, so its start was ended'));
438
+ return code;
439
+ });
440
+ };
441
+ const sayReady = (): void => { if (twinsPending) readyHeld = true; else io.out('managed infrastructure ready\n'); };
442
+ const twinsPhase = async (): Promise<number> => {
443
+ if (!managed) return 0;
444
+ for (const { twin, cli, services } of twins) {
445
+ if (phase === 'up') {
446
+ io.out(`managed infrastructure backing: ${twin.id} twin (containerless)\n`);
447
+ try {
448
+ await managed.twinBackingUp(twin, services, worldData!, cli, launch);
449
+ } catch (error) {
450
+ io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
451
+ return 1;
452
+ }
453
+ } else if (phase === 'status') {
454
+ const ready = await managed.twinBackingStatus(twin, services, worldData!);
455
+ if (ready !== services.length) {
456
+ io.err(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
457
+ return 1;
458
+ }
459
+ } else {
460
+ await managed.twinBackingDown(twin, services, worldData!);
324
461
  }
325
- } else {
326
- await redisTwinDown(twinned, worldData!);
327
462
  }
463
+ return 0;
464
+ };
465
+ if (phase === 'up' && twins.length > 0) twinsPending = true;
466
+ else { const code = await twinsPhase(); if (code !== 0) return code; }
467
+ const databasePhase = async (): Promise<number> => {
468
+ if (mariaDB.length) {
469
+ const { nativeMariaDBUp, nativeMariaDBStatus, nativeMariaDBDown } = await import('./native-mariadb-backing.ts');
470
+ if (mariaDB.length !== 1) { io.err('managed MariaDB needs one explicit native provider per World\n'); return 1; }
471
+ try {
472
+ if (phase === 'up') {
473
+ io.out(`managed infrastructure backing: native MariaDB ${mariaDB[0]!.reference.release}, exact declared provider\n`);
474
+ await nativeMariaDBUp(mariaDB[0]!, worldData, worldConfig, launch);
475
+ } else if (phase === 'status') {
476
+ if (!await nativeMariaDBStatus(mariaDB[0]!, worldData, worldConfig, launch)) {
477
+ io.err('managed MariaDB status: owned server, installed supply or authenticated readiness is not confirmed\n');
478
+ return 1;
479
+ }
480
+ } else await nativeMariaDBDown(worldData);
481
+ } catch (error) { io.err(`managed MariaDB ${phase} failed: ${diagnostic(errorMessage(error))}\n`); return 1; }
328
482
  }
329
483
  if (composed?.length === 0) {
330
- if (phase === 'up') io.out('managed infrastructure ready\n');
331
- else if (phase === 'status') io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() })}\n`);
484
+ if (phase === 'up') sayReady();
485
+ else if (phase === 'status') io.out(`${JSON.stringify(twinServedStatus())}\n`);
332
486
  else io.out('managed infrastructure stopped\n');
333
487
  return 0;
334
488
  }
335
489
 
490
+ // How long the choice of backing took, and how far into the phase it was made (S2, Twenty's World up): Twenty's up spent 220 ms between
491
+ // its infrastructure starting beside the host and the backing's line, none of it said.
492
+ const choosing = Date.now();
336
493
  const backing = await containerlessPostgres(await selectBacking());
494
+ const chosen = `backing chosen in ${Date.now() - choosing} ms, ${Date.now() - phaseAsked} ms into the phase`;
337
495
  if (typeof backing === 'number') return backing;
338
496
  if (backing === 'pglite') return runPglite();
339
497
  if (backing === 'native') {
340
498
  const { nativePostgresUp, nativePostgresStatus, nativePostgresDown } = await import('./native-postgres-backing.ts');
341
499
  const postgres = declared.filter(s => s.kind === 'postgres');
342
- const others = declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s));
500
+ const others = declared.filter(s => s.kind !== 'postgres' && !s.nativeMariaDB && !twinned.includes(s));
343
501
  const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import('./pglite-backing.ts');
344
502
  if (postgres.length !== 1 || unsupportedKinds(others).length) {
345
503
  io.err('managed infrastructure native backing requires one postgres and only supported containerless services alongside it\n');
346
504
  return 1;
347
505
  }
506
+ /** The native backing's status: its containerless others serving and the postmaster's own proof (ownership, its
507
+ * live group and identity, its port answering), then the status line; undefined when either does not hold. */
508
+ const nativeStatus = async (): Promise<string | undefined> => {
509
+ const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
510
+ if (!ready || !await nativePostgresStatus(worldData, signal)) return undefined;
511
+ postgresData = join(worldData, 'native-postgres-data');
512
+ return `${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`;
513
+ };
348
514
  try {
349
515
  if (phase === 'up') {
350
- io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')}\n`);
351
- await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
516
+ io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')} (${chosen})\n`);
517
+ await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), { ...launch, signal: launch.signal ? AbortSignal.any([launch.signal, twinFailed.signal]) : twinFailed.signal, onPlaced: startTwinsBeside });
352
518
  try { await pgliteUp(others, worldData, launch); }
353
519
  catch (error) {
354
520
  try { await nativePostgresDown(worldData); }
355
521
  catch (cleanup) { throw new AggregateError([error, cleanup], 'managed infrastructure startup and native cleanup failed'); }
356
522
  throw error;
357
523
  }
358
- io.out('managed infrastructure ready\n');
524
+ nativeProof = nativeStatus;
525
+ sayReady();
359
526
  } else if (phase === 'status') {
360
- const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
361
- if (!ready || !await nativePostgresStatus(worldData)) return 1;
362
- postgresData = join(worldData, 'native-postgres-data');
363
- io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`);
527
+ const text = await nativeStatus();
528
+ if (text === undefined) return 1;
529
+ io.out(text);
364
530
  } else {
365
531
  await pgliteDown(others, worldData);
366
- await nativePostgresDown(worldData);
532
+ await nativePostgresDown(worldData, 'infra down');
533
+ worldEvent({ event: 'world.infra.down', backing: 'native', ms: Date.now() - phaseAsked });
367
534
  io.out('managed infrastructure stopped\n');
368
535
  }
369
536
  return 0;
@@ -379,7 +546,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
379
546
  if (forcedBy('docker')) io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
380
547
  const result = await run(['up', '-d', '--wait', ...only]);
381
548
  if (result.status !== 0) return fail(result);
382
- io.out('managed infrastructure ready\n');
549
+ sayReady();
383
550
  } else if (phase === 'status') {
384
551
  const expected = await run(['config', '--services']);
385
552
  if (expected.status !== 0) return fail(expected);
@@ -398,4 +565,42 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
398
565
  io.out('managed infrastructure stopped\n');
399
566
  }
400
567
  return 0;
568
+ };
569
+ let code: number;
570
+ try { code = await databasePhase(); }
571
+ catch (error) {
572
+ if (phase === 'up' && mariaDB.length) {
573
+ try {
574
+ if (await runInfrastructure('down', env, { out: () => {}, err: io.err }) !== 0)
575
+ throw new Error('managed infrastructure failed-start teardown is unconfirmed');
576
+ } catch (cleanup) { throw new AggregateError([error, cleanup], 'native MariaDB sibling startup and retirement failed'); }
577
+ }
578
+ throw error;
579
+ }
580
+ if (code !== 0 && phase === 'up' && mariaDB.length) {
581
+ try {
582
+ if (await runInfrastructure('down', env, { out: () => {}, err: io.err }) !== 0)
583
+ io.err('managed infrastructure failed-start teardown is unconfirmed; ownership retained\n');
584
+ } catch (cleanup) { io.err(`managed MariaDB failed-start retirement: ${diagnostic(errorMessage(cleanup))}\n`); }
585
+ }
586
+ if (!twinsPending) { if (code === 0) await handOver(); return code; }
587
+ twinsPending = false;
588
+ if (code !== 0) {
589
+ // A database that failed after its twins began: they finish starting or fail (each retires its own half-started
590
+ // child, twinBackingUp), then come down with the rest, as when a twin fails, so no twin's process or receipt stays.
591
+ if (twinsBeside) {
592
+ await twinsBeside;
593
+ try { await runInfrastructure('down', env, { out: () => {}, err: io.err }, signal); } catch { /* the failure above is the one to report */ }
594
+ }
595
+ return code;
596
+ }
597
+ const twinsCode = await (twinsBeside ?? twinsPhase());
598
+ if (twinsCode !== 0) {
599
+ // The database came up and a twin did not: nothing of this `up` is left serving.
600
+ try { await runInfrastructure('down', env, { out: () => {}, err: io.err }, signal); } catch { /* the failure above is the one to report */ }
601
+ return twinsCode;
602
+ }
603
+ await handOver();
604
+ if (readyHeld) io.out('managed infrastructure ready\n');
605
+ return 0;
401
606
  }