@volter/world-runtime 3.0.163 → 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 (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 +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 +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 +4 -3
  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 +416 -17
  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
package/dist/src/infra.js CHANGED
@@ -7,8 +7,9 @@
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.js";
12
13
  import { signalChild } from './process-signal.mjs';
13
14
  import { spawn } from 'node:child_process';
14
15
  import { fileURLToPath } from 'node:url';
@@ -142,11 +143,48 @@ export async function runInfrastructure(phase, env, io, signal) {
142
143
  io.err('managed infrastructure: missing World lifecycle context\n');
143
144
  return 2;
144
145
  }
146
+ // What this command does to a backing's process is said in the World's own log, beside its services' events.
147
+ setWorldEventsFile(join(dirname(worldData), 'logs', 'events.jsonl'));
148
+ const phaseAsked = Date.now();
149
+ // Retirement follows retained ownership even if the definition or supplier
150
+ // can no longer be read. Other infrastructure still reports its own refusal.
151
+ let retainedMariaDB = false;
152
+ if (phase === 'down') {
153
+ try {
154
+ retainedMariaDB = existsSync(join(worldData, 'native-mariadb.owner.json'))
155
+ || existsSync(join(worldData, 'native-mariadb.owner.json.spawn.json'))
156
+ || existsSync(join(worldData, 'native-mariadb.starting.json'))
157
+ // install-db can outlive its caller before any server receipt exists.
158
+ // Either half of a helper pair enters retirement; a missing birth
159
+ // token remains an explicit uncertainty in nativeMariaDBDown.
160
+ || (existsSync(worldData) && readdirSync(worldData).some(name => /^native-mariadb-tool-.*\.json(?:\.spawn\.json)?$/u.test(name)));
161
+ }
162
+ catch (error) {
163
+ io.err(`managed MariaDB ownership inventory is unreadable: ${errorMessage(error)}\n`);
164
+ return 1;
165
+ }
166
+ }
167
+ if (retainedMariaDB) {
168
+ try {
169
+ await (await import("./native-mariadb-backing.js")).nativeMariaDBDown(worldData);
170
+ }
171
+ catch (error) {
172
+ io.err(`managed MariaDB down failed: ${errorMessage(error)}\n`);
173
+ return 1;
174
+ }
175
+ }
145
176
  const definition = join(dirname(worldConfig), 'world.infrastructure.yml');
146
177
  if (!existsSync(definition)) {
147
178
  io.err('managed infrastructure: declared definition is missing\n');
148
179
  return 2;
149
180
  }
181
+ const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
182
+ // Both the CLI and the World's in-process up enter here. Endpoint metadata cannot supply host DNS or a native
183
+ // default-port listener: refuse before choosing or starting any backing when the substrate carrier is absent.
184
+ if (phase === 'up' && declared.some(service => service.virtualTcpEndpoint) && env.VOLTER_WORLD_VIRTUAL_TCP !== '1') {
185
+ io.err('managed infrastructure up: virtual TCP endpoints require the substrate network carrier; native host DNS/default ports are not supplied\n');
186
+ return 2;
187
+ }
150
188
  // A development World says so every time it comes up: its use is never silent.
151
189
  if (phase === 'up' && developmentMigratesAtRuntime(worldConfig))
152
190
  io.out(`${DEVELOPMENT_MIGRATION_NOTICE}\n`);
@@ -244,7 +282,7 @@ export async function runInfrastructure(phase, env, io, signal) {
244
282
  const { unsupportedKinds } = await import("./pglite-backing.js");
245
283
  const { nativePostgresTools } = await import("./native-postgres-backing.js");
246
284
  const postgres = declared.filter(s => s.kind === 'postgres');
247
- if (postgres.length !== 1 || unsupportedKinds(declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s))).length)
285
+ if (postgres.length !== 1 || unsupportedKinds(declared.filter(s => s.kind !== 'postgres' && !s.nativeMariaDB && !twinned.includes(s))).length)
248
286
  return selected;
249
287
  const native = existsSync(join(worldData, 'native-postgres-data'));
250
288
  const pglite = existsSync(join(worldData, 'pglite-postgres-data'));
@@ -287,7 +325,7 @@ export async function runInfrastructure(phase, env, io, signal) {
287
325
  io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
288
326
  return 1;
289
327
  }
290
- io.out('managed infrastructure ready\n');
328
+ sayReady();
291
329
  return 0;
292
330
  }
293
331
  if (phase === 'status') {
@@ -305,18 +343,76 @@ export async function runInfrastructure(phase, env, io, signal) {
305
343
  io.out('managed infrastructure stopped\n');
306
344
  return 0;
307
345
  }
308
- // ---- redis: the twin, containerless ----------------------------------------
309
- // A redis service is served by the redis twin (redis-backing.ts) on every machine, container runtime or not,
310
- // unless the operator forces the container backing; the other services take the backing chosen above. Where the
311
- // twin is not installed (@volter/twin-redis is not this runtime's dependency), redis goes to that backing too:
312
- // the container where there is one, else the backing's refusal naming redis.
313
- const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
314
- const { redisTwinCli } = await import("./redis-backing.js");
315
- const twinCli = redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)]);
316
- const twinned = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || twinCli === undefined ? [] : declared.filter((s) => s.kind === 'redis');
346
+ // ---- a kind a twin serves: the twin, containerless --------------------------
347
+ // A service whose kind an installed pack declares (`managedService` in pack facts; twin-backing.ts) is served by
348
+ // that pack on every machine, container runtime or not, unless the operator forces the container backing; the
349
+ // other services take the backing chosen above. Which pack is the pack's declaration, never a name here. Where no
350
+ // pack declares the kind, or the one that does is not installed, the kind goes to that backing too: the container
351
+ // where there is one, else the backing's refusal naming the kind (the vendor's own server for MongoDB).
352
+ // The kinds listed are those whose protocol this runtime can ask its two questions in (ManagedProtocol).
353
+ const mariaDB = declared.flatMap(service => service.nativeMariaDB ? [service.nativeMariaDB] : []);
354
+ if (phase !== 'down' && mariaDB.length && ['docker', 'pglite'].includes(env.VOLTER_WORLD_INFRA_BACKING ?? '')) {
355
+ io.err('managed MariaDB: explicit native selection cannot be replaced by the forced backing\n');
356
+ return 1;
357
+ }
358
+ // Resolve only kinds the definition actually asks for. Looking up a pack before filtering its services made a
359
+ // PostgreSQL-only World scan installed Redis/MongoDB packs and their CLI graphs on every infrastructure phase.
360
+ const kinds = ['redis', 'mongodb', 'clickhouse'].filter(kind => declared.some(service => service.kind === kind));
361
+ const managed = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || kinds.length === 0
362
+ ? undefined : await import("./twin-backing.js");
363
+ const protocols = {};
364
+ if (managed) {
365
+ if (kinds.includes('redis'))
366
+ protocols.redis = (await import("./redis-backing.js")).REDIS_PROTOCOL;
367
+ if (kinds.includes('mongodb'))
368
+ protocols.mongodb = (await import("./mongodb-backing.js")).MONGODB_PROTOCOL;
369
+ if (kinds.includes('clickhouse'))
370
+ protocols.clickhouse = (await import("./clickhouse-backing.js")).CLICKHOUSE_PROTOCOL;
371
+ }
372
+ const expectedTwinPackages = new Map();
373
+ const twins = !managed ? [] : kinds.flatMap((kind) => {
374
+ const services = declared.filter((s) => s.kind === kind);
375
+ const beside = [dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)];
376
+ const twin = managed.managedTwin(kind, protocols[kind], beside, phase !== 'down' && services.some(service => service.originalInputs !== undefined));
377
+ if (twin)
378
+ expectedTwinPackages.set(kind, twin.package);
379
+ const cli = twin && managed.twinBackingCli(twin, beside);
380
+ return twin && (cli || phase === 'down') ? [{ twin, cli: cli ?? '', services }] : [];
381
+ });
382
+ const twinned = twins.flatMap((served) => served.services);
383
+ // A declared original-input carrier cannot fall through to a backing that drops it.
384
+ // Down still retires established owners even when a pack or source snapshot is unavailable.
385
+ if (phase !== 'down') {
386
+ try {
387
+ for (const service of declared)
388
+ if (service.originalInputs) {
389
+ const consumer = twins.find(entry => entry.services.includes(service));
390
+ if (!managed || !consumer)
391
+ throw new Error(`managed ${service.kind} has original inputs and no installed declaring consumer`);
392
+ managed.originalInputArguments(consumer.twin, service, worldConfig);
393
+ }
394
+ }
395
+ catch (error) {
396
+ io.err(`managed infrastructure ${phase} refused original inputs: ${diagnostic(errorMessage(error))}\n`);
397
+ return 1;
398
+ }
399
+ }
317
400
  /** The services the backing below answers for: every one, or those the twin does not serve (by compose service name). */
318
- const native = declared.filter((s) => s.kind === 'mongodb');
319
- const composed = twinned.length === 0 && native.length === 0 ? null : declared.filter((s) => !twinned.includes(s) && !native.includes(s)).map((s) => s.kind);
401
+ const native = declared.filter((s) => s.kind === 'mongodb' && !twinned.includes(s));
402
+ // Tab hosts expose their kernel tree on this symbol (world-store.ts). Redis/MongoDB's managed kinds need
403
+ // their installed declaring twins there; a missing pack is an input refusal, never a native-server attempt.
404
+ // PostgreSQL is separate: its available WALI server tools or PGlite still own that declared backing.
405
+ const inTab = process[Symbol.for('tabnode.run.vfs')] !== undefined;
406
+ if (inTab && phase !== 'down') {
407
+ const missing = kinds.filter(kind => !twinned.some(service => service.kind === kind)
408
+ && (env.VOLTER_WORLD_INFRA_BACKING !== 'docker' || native.some(service => service.kind === kind)));
409
+ if (missing.length) {
410
+ for (const kind of missing)
411
+ io.err(`managed ${kind} needs ${expectedTwinPackages.get(kind) ?? `@volter/twin-${kind}`} installed in this tab World\n`);
412
+ return 1;
413
+ }
414
+ }
415
+ 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);
320
416
  // A database's native wire and storage are its vendor server's. MongoDB never needs a Docker probe or container.
321
417
  if (native.length > 0) {
322
418
  const { pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
@@ -339,119 +435,293 @@ export async function runInfrastructure(phase, env, io, signal) {
339
435
  return 1;
340
436
  }
341
437
  }
342
- if (twinned.length > 0) {
343
- const { redisTwinDown, redisTwinStatus, redisTwinUp } = await import("./redis-backing.js");
344
- const cli = twinCli;
345
- if (phase === 'up') {
346
- io.out('managed infrastructure backing: redis twin (containerless)\n');
438
+ // THE TWIN-SERVED KINDS WAIT FOR THE DATABASE'S CLUSTER PLACEMENT. A store staged with a prepared cluster is placed where its
439
+ // twin serves from, and only beside a database that carries the same migration (takePreparedStore). Brought up
440
+ // first, a twin's store was placed before the database's backing was chosen, and where that backing then
441
+ // declined the prepared cluster (its programs' stamp, PGlite serving a native cluster) the World had an
442
+ // unprepared database and a prepared store. On `up` with the native backing, its onPlaced starts the twins once
443
+ // the cluster is placed, beside readiness; otherwise they start after the database. The live cluster is then
444
+ // there to be read; `status` and `down` keep their order. The ready line is said once,
445
+ // after both.
446
+ /** What `status` prints when every declared service is twin-served: one fact, said by status and handed over by up. */
447
+ const twinServedStatus = () => ({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() });
448
+ /** The native backing's status, its proof asked as status asks it: set by the native branch's `up` once it is ready. */
449
+ let nativeProof;
450
+ /** What this `up` has proved, handed to the runtime's own in-process up (InfrastructureIo.verified) as the status its
451
+ * `status` would print, its proofs asked again now: every twin held (twinBackingHeld), and then either every service
452
+ * twin-served or, with no MongoDB native, the native backing's own status proof (nativeStatus: the containerless
453
+ * others serving, pgliteStatus, and the postmaster's, nativePostgresStatus). It does not repeat status's choice of
454
+ * backing (selectBacking, a `docker info` probe): the backing is the one this up just brought up. Any doubt hands
455
+ * nothing over, and the runtime's probe runs. */
456
+ const handOver = async () => {
457
+ if (!io.verified || phase !== 'up' || native.length !== 0)
458
+ return;
459
+ for (const { twin, services } of twins)
460
+ if (!await managed.twinBackingHeld(twin, services, worldData))
461
+ return;
462
+ if (mariaDB.length && !await (await import("./native-mariadb-backing.js")).nativeMariaDBStatus(mariaDB[0], worldData, worldConfig, launch))
463
+ return;
464
+ if (composed?.length === 0) {
465
+ io.verified(`${JSON.stringify(twinServedStatus())}\n`);
466
+ return;
467
+ }
468
+ if (!nativeProof)
469
+ return;
470
+ let text;
471
+ try {
472
+ text = await nativeProof();
473
+ }
474
+ catch {
475
+ return;
476
+ }
477
+ if (text)
478
+ io.verified(text);
479
+ };
480
+ let twinsPending = false;
481
+ let readyHeld = false;
482
+ // THE TWINS COME UP BESIDE THE DATABASE'S READINESS (S2 R79), not after it: Twenty's redis twin began only when the
483
+ // postmaster answered (+1.36 s) and held up 330 ms more. 840a1ae3's rule stands: a twin's staged store is placed beside
484
+ // the cluster the backing chose, so the twins start no earlier than the native backing's onPlaced, said after the
485
+ // cluster is placed (or made by initdb) and its server spawned. Every other backing still runs them after it returns.
486
+ let twinsBeside;
487
+ // A twin that fails beside the readiness wait ends that wait (its signal), so the up fails then, not at the
488
+ // database's readiness or deadline: the native start retires its postmaster as on any abort, and the failed-database
489
+ // path below brings the rest down.
490
+ const twinFailed = new AbortController();
491
+ const startTwinsBeside = () => {
492
+ if (!twinsPending || twinsBeside)
493
+ return;
494
+ io.out('managed infrastructure: twins started beside the database\'s readiness (its cluster placed)\n');
495
+ twinsBeside = twinsPhase().then((code) => {
496
+ if (code !== 0)
497
+ twinFailed.abort(new Error('a twin failed beside the database\'s readiness, so its start was ended'));
498
+ return code;
499
+ });
500
+ };
501
+ const sayReady = () => { if (twinsPending)
502
+ readyHeld = true;
503
+ else
504
+ io.out('managed infrastructure ready\n'); };
505
+ const twinsPhase = async () => {
506
+ if (!managed)
507
+ return 0;
508
+ for (const { twin, cli, services } of twins) {
509
+ if (phase === 'up') {
510
+ io.out(`managed infrastructure backing: ${twin.id} twin (containerless)\n`);
511
+ try {
512
+ await managed.twinBackingUp(twin, services, worldData, cli, launch);
513
+ }
514
+ catch (error) {
515
+ io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
516
+ return 1;
517
+ }
518
+ }
519
+ else if (phase === 'status') {
520
+ const ready = await managed.twinBackingStatus(twin, services, worldData);
521
+ if (ready !== services.length) {
522
+ io.err(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
523
+ return 1;
524
+ }
525
+ }
526
+ else {
527
+ await managed.twinBackingDown(twin, services, worldData);
528
+ }
529
+ }
530
+ return 0;
531
+ };
532
+ if (phase === 'up' && twins.length > 0)
533
+ twinsPending = true;
534
+ else {
535
+ const code = await twinsPhase();
536
+ if (code !== 0)
537
+ return code;
538
+ }
539
+ const databasePhase = async () => {
540
+ if (mariaDB.length) {
541
+ const { nativeMariaDBUp, nativeMariaDBStatus, nativeMariaDBDown } = await import("./native-mariadb-backing.js");
542
+ if (mariaDB.length !== 1) {
543
+ io.err('managed MariaDB needs one explicit native provider per World\n');
544
+ return 1;
545
+ }
347
546
  try {
348
- await redisTwinUp(twinned, worldData, cli, launch);
547
+ if (phase === 'up') {
548
+ io.out(`managed infrastructure backing: native MariaDB ${mariaDB[0].reference.release}, exact declared provider\n`);
549
+ await nativeMariaDBUp(mariaDB[0], worldData, worldConfig, launch);
550
+ }
551
+ else if (phase === 'status') {
552
+ if (!await nativeMariaDBStatus(mariaDB[0], worldData, worldConfig, launch)) {
553
+ io.err('managed MariaDB status: owned server, installed supply or authenticated readiness is not confirmed\n');
554
+ return 1;
555
+ }
556
+ }
557
+ else
558
+ await nativeMariaDBDown(worldData);
349
559
  }
350
560
  catch (error) {
351
- io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
561
+ io.err(`managed MariaDB ${phase} failed: ${diagnostic(errorMessage(error))}\n`);
352
562
  return 1;
353
563
  }
354
564
  }
565
+ if (composed?.length === 0) {
566
+ if (phase === 'up')
567
+ sayReady();
568
+ else if (phase === 'status')
569
+ io.out(`${JSON.stringify(twinServedStatus())}\n`);
570
+ else
571
+ io.out('managed infrastructure stopped\n');
572
+ return 0;
573
+ }
574
+ // 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
575
+ // its infrastructure starting beside the host and the backing's line, none of it said.
576
+ const choosing = Date.now();
577
+ const backing = await containerlessPostgres(await selectBacking());
578
+ const chosen = `backing chosen in ${Date.now() - choosing} ms, ${Date.now() - phaseAsked} ms into the phase`;
579
+ if (typeof backing === 'number')
580
+ return backing;
581
+ if (backing === 'pglite')
582
+ return runPglite();
583
+ if (backing === 'native') {
584
+ const { nativePostgresUp, nativePostgresStatus, nativePostgresDown } = await import("./native-postgres-backing.js");
585
+ const postgres = declared.filter(s => s.kind === 'postgres');
586
+ const others = declared.filter(s => s.kind !== 'postgres' && !s.nativeMariaDB && !twinned.includes(s));
587
+ const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
588
+ if (postgres.length !== 1 || unsupportedKinds(others).length) {
589
+ io.err('managed infrastructure native backing requires one postgres and only supported containerless services alongside it\n');
590
+ return 1;
591
+ }
592
+ /** The native backing's status: its containerless others serving and the postmaster's own proof (ownership, its
593
+ * live group and identity, its port answering), then the status line; undefined when either does not hold. */
594
+ const nativeStatus = async () => {
595
+ const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
596
+ if (!ready || !await nativePostgresStatus(worldData, signal))
597
+ return undefined;
598
+ postgresData = join(worldData, 'native-postgres-data');
599
+ return `${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`;
600
+ };
601
+ try {
602
+ if (phase === 'up') {
603
+ io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')} (${chosen})\n`);
604
+ await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), { ...launch, signal: launch.signal ? AbortSignal.any([launch.signal, twinFailed.signal]) : twinFailed.signal, onPlaced: startTwinsBeside });
605
+ try {
606
+ await pgliteUp(others, worldData, launch);
607
+ }
608
+ catch (error) {
609
+ try {
610
+ await nativePostgresDown(worldData);
611
+ }
612
+ catch (cleanup) {
613
+ throw new AggregateError([error, cleanup], 'managed infrastructure startup and native cleanup failed');
614
+ }
615
+ throw error;
616
+ }
617
+ nativeProof = nativeStatus;
618
+ sayReady();
619
+ }
620
+ else if (phase === 'status') {
621
+ const text = await nativeStatus();
622
+ if (text === undefined)
623
+ return 1;
624
+ io.out(text);
625
+ }
626
+ else {
627
+ await pgliteDown(others, worldData);
628
+ await nativePostgresDown(worldData, 'infra down');
629
+ worldEvent({ event: 'world.infra.down', backing: 'native', ms: Date.now() - phaseAsked });
630
+ io.out('managed infrastructure stopped\n');
631
+ }
632
+ return 0;
633
+ }
634
+ catch (error) {
635
+ io.err(`managed infrastructure ${phase} failed: ${diagnostic(errorMessage(error))}\n`);
636
+ return 1;
637
+ }
638
+ }
639
+ const only = composed ?? [];
640
+ if (phase === 'up') {
641
+ if (forcedBy('docker'))
642
+ io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
643
+ const result = await run(['up', '-d', '--wait', ...only]);
644
+ if (result.status !== 0)
645
+ return fail(result);
646
+ sayReady();
647
+ }
355
648
  else if (phase === 'status') {
356
- const ready = await redisTwinStatus(twinned, worldData);
357
- if (ready !== twinned.length) {
358
- io.err(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
649
+ const expected = await run(['config', '--services']);
650
+ if (expected.status !== 0)
651
+ return fail(expected);
652
+ const running = await run(['ps', '--status', 'running', '--services']);
653
+ if (running.status !== 0)
654
+ return fail(running);
655
+ const expectedNames = expected.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
656
+ const runningNames = running.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
657
+ if (expectedNames.length === 0 || expectedNames.join('\0') !== runningNames.join('\0')) {
658
+ io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length + native.length}/${expectedNames.length + twinned.length + native.length} declared services are ready\n`);
359
659
  return 1;
360
660
  }
661
+ io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env), ...backingFact('docker'), ...databaseFacts() })}\n`);
361
662
  }
362
663
  else {
363
- await redisTwinDown(twinned, worldData);
364
- }
365
- }
366
- if (composed?.length === 0) {
367
- if (phase === 'up')
368
- io.out('managed infrastructure ready\n');
369
- else if (phase === 'status')
370
- io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() })}\n`);
371
- else
664
+ const result = await run(['down', '--remove-orphans']);
665
+ if (result.status !== 0)
666
+ return fail(result);
372
667
  io.out('managed infrastructure stopped\n');
668
+ }
373
669
  return 0;
670
+ };
671
+ let code;
672
+ try {
673
+ code = await databasePhase();
374
674
  }
375
- const backing = await containerlessPostgres(await selectBacking());
376
- if (typeof backing === 'number')
377
- return backing;
378
- if (backing === 'pglite')
379
- return runPglite();
380
- if (backing === 'native') {
381
- const { nativePostgresUp, nativePostgresStatus, nativePostgresDown } = await import("./native-postgres-backing.js");
382
- const postgres = declared.filter(s => s.kind === 'postgres');
383
- const others = declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s));
384
- const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
385
- if (postgres.length !== 1 || unsupportedKinds(others).length) {
386
- io.err('managed infrastructure native backing requires one postgres and only supported containerless services alongside it\n');
387
- return 1;
388
- }
389
- try {
390
- if (phase === 'up') {
391
- io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')}\n`);
392
- await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
393
- try {
394
- await pgliteUp(others, worldData, launch);
395
- }
396
- catch (error) {
397
- try {
398
- await nativePostgresDown(worldData);
399
- }
400
- catch (cleanup) {
401
- throw new AggregateError([error, cleanup], 'managed infrastructure startup and native cleanup failed');
402
- }
403
- throw error;
404
- }
405
- io.out('managed infrastructure ready\n');
406
- }
407
- else if (phase === 'status') {
408
- const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
409
- if (!ready || !await nativePostgresStatus(worldData))
410
- return 1;
411
- postgresData = join(worldData, 'native-postgres-data');
412
- io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`);
675
+ catch (error) {
676
+ if (phase === 'up' && mariaDB.length) {
677
+ try {
678
+ if (await runInfrastructure('down', env, { out: () => { }, err: io.err }) !== 0)
679
+ throw new Error('managed infrastructure failed-start teardown is unconfirmed');
413
680
  }
414
- else {
415
- await pgliteDown(others, worldData);
416
- await nativePostgresDown(worldData);
417
- io.out('managed infrastructure stopped\n');
681
+ catch (cleanup) {
682
+ throw new AggregateError([error, cleanup], 'native MariaDB sibling startup and retirement failed');
418
683
  }
419
- return 0;
420
684
  }
421
- catch (error) {
422
- io.err(`managed infrastructure ${phase} failed: ${diagnostic(errorMessage(error))}\n`);
423
- return 1;
685
+ throw error;
686
+ }
687
+ if (code !== 0 && phase === 'up' && mariaDB.length) {
688
+ try {
689
+ if (await runInfrastructure('down', env, { out: () => { }, err: io.err }) !== 0)
690
+ io.err('managed infrastructure failed-start teardown is unconfirmed; ownership retained\n');
691
+ }
692
+ catch (cleanup) {
693
+ io.err(`managed MariaDB failed-start retirement: ${diagnostic(errorMessage(cleanup))}\n`);
424
694
  }
425
695
  }
426
- const only = composed ?? [];
427
- if (phase === 'up') {
428
- if (forcedBy('docker'))
429
- io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
430
- const result = await run(['up', '-d', '--wait', ...only]);
431
- if (result.status !== 0)
432
- return fail(result);
433
- io.out('managed infrastructure ready\n');
696
+ if (!twinsPending) {
697
+ if (code === 0)
698
+ await handOver();
699
+ return code;
434
700
  }
435
- else if (phase === 'status') {
436
- const expected = await run(['config', '--services']);
437
- if (expected.status !== 0)
438
- return fail(expected);
439
- const running = await run(['ps', '--status', 'running', '--services']);
440
- if (running.status !== 0)
441
- return fail(running);
442
- const expectedNames = expected.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
443
- const runningNames = running.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
444
- if (expectedNames.length === 0 || expectedNames.join('\0') !== runningNames.join('\0')) {
445
- io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length + native.length}/${expectedNames.length + twinned.length + native.length} declared services are ready\n`);
446
- return 1;
701
+ twinsPending = false;
702
+ if (code !== 0) {
703
+ // A database that failed after its twins began: they finish starting or fail (each retires its own half-started
704
+ // child, twinBackingUp), then come down with the rest, as when a twin fails, so no twin's process or receipt stays.
705
+ if (twinsBeside) {
706
+ await twinsBeside;
707
+ try {
708
+ await runInfrastructure('down', env, { out: () => { }, err: io.err }, signal);
709
+ }
710
+ catch { /* the failure above is the one to report */ }
447
711
  }
448
- io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env), ...backingFact('docker'), ...databaseFacts() })}\n`);
712
+ return code;
449
713
  }
450
- else {
451
- const result = await run(['down', '--remove-orphans']);
452
- if (result.status !== 0)
453
- return fail(result);
454
- io.out('managed infrastructure stopped\n');
714
+ const twinsCode = await (twinsBeside ?? twinsPhase());
715
+ if (twinsCode !== 0) {
716
+ // The database came up and a twin did not: nothing of this `up` is left serving.
717
+ try {
718
+ await runInfrastructure('down', env, { out: () => { }, err: io.err }, signal);
719
+ }
720
+ catch { /* the failure above is the one to report */ }
721
+ return twinsCode;
455
722
  }
723
+ await handOver();
724
+ if (readyHeld)
725
+ io.out('managed infrastructure ready\n');
456
726
  return 0;
457
727
  }
@@ -1,3 +1,6 @@
1
+ import { type NativeServerInputs } from './native-server-inputs.js';
2
+ import { type GoMysqlBinding } from './go-mysql-binding.js';
3
+ import { type OriginalInputReference } from './original-inputs.js';
1
4
  import { type CoverageReport } from './covers.js';
2
5
  import { type ProjectMigrationTool } from './project-inspect.js';
3
6
  import { type WorldConfig, type WorldServiceConfig } from './schema.js';
@@ -84,9 +87,31 @@ export type InitInfraStub = {
84
87
  export type InitComposeService = {
85
88
  kind: string;
86
89
  image: string;
90
+ /** An explicit MongoDB connection mode, also carried to the selected backing. */
91
+ replicaSet?: string;
87
92
  /** deterministic host port, derived from the world name (see composePort) */
88
93
  hostPort: number;
89
94
  containerPort: number;
95
+ virtualTcpEndpoint?: {
96
+ version: 1;
97
+ host: string;
98
+ port: number;
99
+ targetPort: number;
100
+ };
101
+ connectionBindings?: Record<string, GoMysqlBinding & {
102
+ containerPort: number;
103
+ query?: string;
104
+ }>;
105
+ sides?: Record<string, {
106
+ hostPort: number;
107
+ containerPort: number;
108
+ }>;
109
+ configuration?: Record<string, unknown>;
110
+ originalInputs?: OriginalInputReference;
111
+ originalInputsFrom?: string;
112
+ nativeServerInputs?: NativeServerInputs;
113
+ nativeServerInputsFrom?: string;
114
+ nativeCredentialAliases?: string[];
90
115
  memoryMiB: number;
91
116
  /** compose volume key, or null (the on-disk docker volume is `<project>_<key>` — the compose
92
117
  * project name carries the world name, so volumes are named per world) */
@@ -118,6 +143,12 @@ export type InitPlan = {
118
143
  * (runtime.database.prepare): reported, never written, and no part of the vendor-coverage verdict */
119
144
  undeclaredMigrations: ProjectMigrationTool[];
120
145
  infra: InitInfraStub[];
146
+ /** each service of the project's own stack and what was made of it, where `--needs` gave the stack */
147
+ stackNeeds?: InitStackNeed[];
148
+ /** The same declared application names used by selection and its coverage proof. */
149
+ stackEnvironmentNames?: string[];
150
+ /** the project files a World made for an image left unread: the source does not hold them, so they are the person's own */
151
+ leftAsThePersonsOwn?: string[];
121
152
  /** the services emitted into the private managed-infrastructure definition */
122
153
  compose: InitComposeService[];
123
154
  /** private managed-infrastructure definition, or null when no detected infra has a recipe */
@@ -195,6 +226,21 @@ export type InitOptions = {
195
226
  sources?: Record<string, string>;
196
227
  /** A world with no app: served as `/<org>/<world>/`. */
197
228
  bare?: string;
229
+ /** The project's own stack as its image builder read it (`needs.json`): architecture, "The project's own stack as evidence". */
230
+ needs?: string;
231
+ };
232
+ /** One service of the project's stack, as `init` took it. */
233
+ export type InitStackNeed = {
234
+ service: string;
235
+ image?: string;
236
+ family?: string;
237
+ /** the managed kind it is, or null where the World has none for it */
238
+ kind: string | null;
239
+ /** the application's variables given this service's address */
240
+ names: string[];
241
+ /** true where the stack named no variable and the conventional names were supplied */
242
+ conventional: boolean;
243
+ note: string;
198
244
  };
199
245
  /** The twins available at `root`, sorted — a deterministic input (catalog.ts: this checkout's
200
246
  * vendor directories in the pack repository, or the `@volter/twin-<vendor>` packages installed above root). */