@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
@@ -16,75 +16,17 @@ import { appendFileSync, closeSync, copyFileSync, cpSync, existsSync, lstatSync,
16
16
  import { createHash, randomUUID } from 'node:crypto';
17
17
  import { assertWorldConfig } from "./schema.js";
18
18
  import { connect } from 'node:net';
19
- import { dirname, join } from 'node:path';
19
+ import { delimiter, dirname, join } from 'node:path';
20
20
  import { tmpdir } from 'node:os';
21
21
  import { spawn } from 'node:child_process';
22
- import { commandLifetime, COMMAND_GRACE_MS, COMMAND_CONFIRM_MS } from "./command-lifetime.js";
23
- import { survivingOwnedGroups } from "./process-groups.js";
22
+ import { commandLifetime, COMMAND_CONFIRM_MS } from "./command-lifetime.js";
23
+ import { survivingOwnedGroups, worldEvent } from "./process-groups.js";
24
24
  import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess } from "./process-receipt.js";
25
25
  import { errorMessage } from "./error-message.js";
26
26
  import { runtimeManifest } from "./package-manifest.js";
27
27
  import { fileURLToPath } from 'node:url';
28
- import { parse } from 'yaml';
29
28
  import { mongodbArgv, mongodbReady, mongodbServes } from "./mongodb-backing.js";
30
- /** Read the generated definition's loopback port mappings. MongoDB may explicitly request
31
- * one replica-set member through its Compose-compatible command; other mongod options are refused. */
32
- export function parseInfraDefinition(text) {
33
- const definition = parse(text);
34
- const services = [];
35
- for (const [kind, value] of Object.entries(definition?.services ?? {})) {
36
- const declared = value;
37
- let replicaSet;
38
- if (kind === 'mongodb' && declared.command !== undefined) {
39
- const command = declared.command;
40
- if (!Array.isArray(command) || command.length !== 3 || command[0] !== 'mongod' || command[1] !== '--replSet'
41
- || typeof command[2] !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(command[2])) {
42
- throw new Error('World-managed MongoDB command must be ["mongod", "--replSet", "<name>"] or absent for standalone');
43
- }
44
- replicaSet = command[2];
45
- }
46
- const ports = (declared.ports ?? []).flatMap((port) => {
47
- const match = typeof port === 'string' ? port.match(/^127\.0\.0\.1:(\d+):\d+$/) : null;
48
- return match ? [Number(match[1])] : [];
49
- });
50
- if (ports.length !== 1)
51
- throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port`);
52
- let postgres;
53
- if (kind === 'postgres') {
54
- const environment = declared.environment ?? {};
55
- const user = environment.POSTGRES_USER ?? 'postgres';
56
- const database = environment.POSTGRES_DB ?? user;
57
- const password = environment.POSTGRES_PASSWORD;
58
- if ([user, database, password].some(value => value !== undefined && (typeof value !== 'string' || !value || /[\r\n\0]|\$\{/.test(value)))) {
59
- throw new Error('World-managed PGlite requires literal POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD');
60
- }
61
- if ([user, database].some(value => Buffer.byteLength(value, 'utf8') > 63))
62
- throw new Error('World-managed PGlite identifiers must fit PostgreSQL\'s 63-byte name limit');
63
- postgres = { user: user, database: database, ...(password !== undefined ? { password: password } : {}) };
64
- }
65
- services.push({ kind, hostPort: ports[0], ...(replicaSet ? { replicaSet } : {}), ...(postgres ? { postgres } : {}) });
66
- }
67
- return services;
68
- }
69
- /** Endpoints of the declared infrastructure, published through external.discover. Only URLs
70
- * whose protocol and loopback port match a declared service belong to this lifecycle. */
71
- export function infraConnections(services, env) {
72
- const connections = {};
73
- for (const [name, value] of Object.entries(env)) {
74
- if (value === undefined)
75
- continue;
76
- try {
77
- const url = new URL(value);
78
- if (url.hostname !== '127.0.0.1')
79
- continue;
80
- const kind = url.protocol === 'postgresql:' ? 'postgres' : url.protocol.slice(0, -1);
81
- if (services.some((service) => service.kind === kind && service.hostPort === Number(url.port)))
82
- connections[name] = value;
83
- }
84
- catch { /* not an infrastructure endpoint */ }
85
- }
86
- return connections;
87
- }
29
+ export { parseInfraDefinition, infraConnections } from "./infra-definition.js";
88
30
  // this module's directory under Node as under Bun: `volter-world-infra` is a node-shebang bin, where
89
31
  // `import.meta.dir` (Bun's) is undefined
90
32
  // lazy: a module that builds a URL from import.meta.url when it loads fails to load in workerd, which bundles this
@@ -174,7 +116,8 @@ function listeningReceipt(path) {
174
116
  const value = JSON.parse(text);
175
117
  if (value.version !== 1 || !Number.isSafeInteger(value.pid) || !Number.isSafeInteger(value.port))
176
118
  return undefined;
177
- return { pid: value.pid, port: value.port, boot: typeof value.boot === 'string' ? value.boot : null };
119
+ return { pid: value.pid, port: value.port, boot: typeof value.boot === 'string' ? value.boot : null,
120
+ ...(typeof value.openSplit === 'string' ? { openSplit: value.openSplit } : {}) };
178
121
  }
179
122
  catch {
180
123
  return undefined;
@@ -215,6 +158,106 @@ export function preparedCluster(worldConfig) {
215
158
  const directory = join(dirname(worldConfig), PREPARED_DIRECTORY, containerlessDataDirectory('postgres'));
216
159
  return { directory, stamp: `${directory}.json` };
217
160
  }
161
+ /**
162
+ * What a prepared cluster must have been made with: this runtime (whose pglite-host.mjs is the open path that made
163
+ * it), its exact PGlite pin (initdb's encoding and locale are PGlite's defaults: the host passes no initdb
164
+ * parameters), and the declared user and database. The password is not in the cluster: the role is created without
165
+ * one (LOGIN SUPERUSER) and the host's gateway checks the declared password on each connection, so a World with
166
+ * another password takes the same cluster and authenticates against its own. Extensions are loaded into the module
167
+ * at each open, never created in the cluster by the open path.
168
+ */
169
+ /**
170
+ * A managed store other than the cluster that a step run with the World up may have written (architecture, "A step
171
+ * may need the World"): where the preparation stages it, beside the cluster, and its stamp.
172
+ */
173
+ export function preparedStore(worldConfig, name) {
174
+ const directory = join(dirname(worldConfig), PREPARED_DIRECTORY, name);
175
+ return { directory, stamp: `${directory}.json` };
176
+ }
177
+ const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
178
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
179
+ /** The state directory a preparation with the World up runs in, beside the World's own (native-postgres-backing.ts). */
180
+ export const PREPARE_STATE_DIRECTORY = '.volter-prepare';
181
+ /** The stamp of a staged store: the migration it was written by. */
182
+ export function preparedStoreStamp(migration) {
183
+ return `${canonical({ migration })}\n`;
184
+ }
185
+ /**
186
+ * Place a staged store where its service serves from, at `up`: only into a place that holds nothing, and only when
187
+ * its stamp is the migration this World expects (the rule a prepared cluster is taken by). Anything else leaves the
188
+ * service to start empty, and the note says why.
189
+ */
190
+ export function takePreparedStore(worldConfig, env, name, target) {
191
+ if (!worldConfig)
192
+ return { placed: false };
193
+ const { directory, stamp } = preparedStore(worldConfig, name);
194
+ if (!existsSync(directory) || !existsSync(stamp))
195
+ return { placed: false };
196
+ if (existsSync(target) && readdirSync(target).length)
197
+ return { placed: false };
198
+ const wanted = expectedMigration(worldConfig, env, directory);
199
+ if ('note' in wanted)
200
+ return { placed: false, note: wanted.note.replace('prepared cluster', `prepared ${name}`) };
201
+ if (!wanted.migration || readFileSync(stamp, 'utf8') !== preparedStoreStamp(wanted.migration))
202
+ return { placed: false, note: `prepared ${name} ${directory} not taken: its stamp names another migration` };
203
+ // A STORE GOES WITH ITS CLUSTER. Placed beside a database that does not carry the same migration, the World would
204
+ // have an unprepared database and a prepared store, and an application told to run the step itself would run it
205
+ // on a store that already holds what the step wrote. On `up` the twin-served kinds come up after the native
206
+ // backing's onPlaced (cluster placed) or after the database (infra.ts), so the live cluster is there to be read
207
+ // and must carry the migration. Where no cluster is live
208
+ // (a caller that starts a twin alone), a prepared cluster stamped with it must at least be waiting.
209
+ const data = dirname(target), prepared = join(dirname(worldConfig), PREPARED_DIRECTORY);
210
+ const clusters = [...new Set(['native-postgres-data', containerlessDataDirectory('postgres')])];
211
+ const live = clusters.map((cluster) => join(data, cluster)).filter((cluster) => existsSync(join(cluster, 'PG_VERSION')));
212
+ const carried = live.length > 0
213
+ ? live.some((cluster) => liveDatabaseMigration(worldConfig, env, cluster) !== undefined)
214
+ : clusters.map((cluster) => join(prepared, cluster)).some((cluster) => {
215
+ if (!existsSync(join(cluster, 'PG_VERSION')) || !existsSync(`${cluster}.json`))
216
+ return false;
217
+ try {
218
+ return canonical(JSON.parse(readFileSync(`${cluster}.json`, 'utf8')).migration ?? null) === canonical(wanted.migration);
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ });
224
+ if (!carried)
225
+ return { placed: false, note: `prepared ${name} ${directory} not taken: ${live.length > 0 ? 'the live database does not carry its migration' : 'no prepared cluster of its migration is there to be taken with it'}` };
226
+ if (existsSync(target))
227
+ rmdirSync(target);
228
+ mkdirSync(dirname(target), { recursive: true });
229
+ try {
230
+ renameSync(directory, target);
231
+ }
232
+ catch (error) {
233
+ if (error.code !== 'EXDEV')
234
+ throw error;
235
+ cpSync(directory, target, { recursive: true });
236
+ }
237
+ // THE STORE IS READ UNDER THE NAME THIS UP USES. A twin keeps its state inside its root under the state directory's
238
+ // name; the preparation ran with its own (PREPARE_STATE_DIRECTORY) and this `up` runs with another, so the root as
239
+ // it was written is read by nobody. The one the preparation wrote under is renamed to the one the twin is about to
240
+ // be given, and the note says both, or says that nothing was there to rename.
241
+ const wrote = join(target, PREPARE_STATE_DIRECTORY), reads = join(target, env.VOLTER_STATE_DIR || '.volter');
242
+ let under = `nothing in it is under ${PREPARE_STATE_DIRECTORY}`;
243
+ if (existsSync(wrote) && wrote !== reads) {
244
+ if (existsSync(reads))
245
+ return { placed: true, note: `prepared ${name} ${directory} placed at ${target}, and its ${PREPARE_STATE_DIRECTORY} NOT renamed: ${reads} is there already; the service reads that one` };
246
+ renameSync(wrote, reads);
247
+ under = `what the preparation wrote under ${PREPARE_STATE_DIRECTORY} is now under ${env.VOLTER_STATE_DIR || '.volter'}, where this up's service reads`;
248
+ }
249
+ return { placed: true, note: `prepared ${name} ${directory} placed at ${target}; ${under}` };
250
+ }
251
+ /**
252
+ * The twins a step declared `with: "world"` runs beside, each by the version the World names: part of what the
253
+ * cluster was migrated with, so a World whose twins moved does not take a cluster another set of them wrote into.
254
+ * Read from the document itself, in id order.
255
+ */
256
+ export function preparedWorldServices(worldConfig) {
257
+ const document = JSON.parse(readFileSync(worldConfig, 'utf8'));
258
+ const twins = (document.services ?? []).filter((service) => service.type === 'twin' && typeof service.id === 'string');
259
+ return Object.fromEntries(twins.map((service) => [service.id, typeof service.source?.version === 'string' ? service.source.version : 'unversioned']).sort(([a], [b]) => a.localeCompare(b)));
260
+ }
218
261
  function preparedStamp(service, migration) {
219
262
  const manifest = runtimeManifest();
220
263
  const pglite = manifest.dependencies?.['@electric-sql/pglite'];
@@ -223,6 +266,20 @@ function preparedStamp(service, migration) {
223
266
  return `${JSON.stringify({ runtime: manifest.version ?? 'unversioned', pglite, user: service.postgres.user, database: service.postgres.database,
224
267
  ...(migration ? { migration } : {}) })}\n`;
225
268
  }
269
+ const declaredDatabaseReceipt = (serviceData) => join(serviceData, '.volter-declared-database.json');
270
+ /** This cluster served the declared database and role; this is not a migration or process-ownership receipt. */
271
+ function hasDeclaredDatabase(serviceData, identity) {
272
+ if (!existsSync(join(serviceData, 'PG_VERSION')))
273
+ return false;
274
+ try {
275
+ return readFileSync(declaredDatabaseReceipt(serviceData), 'utf8') === identity;
276
+ }
277
+ catch (error) {
278
+ if (error.code === 'ENOENT')
279
+ return false;
280
+ throw error;
281
+ }
282
+ }
226
283
  /**
227
284
  * The SHA-256 of each of a migration step's inputs, under its cwd in the app's tree: a file's bytes; a directory's
228
285
  * sorted relative paths, each with its type, its mode and its bytes (a link, its target). What the image build records
@@ -257,7 +314,7 @@ export function databasePrepareOf(worldConfig) {
257
314
  return assertWorldConfig(JSON.parse(readFileSync(worldConfig, 'utf8')), worldConfig).databasePrepare;
258
315
  }
259
316
  /** `NAME=value` lines (an image's environment, VOLTER_WORLD_APP_ENV), as an environment. */
260
- function environmentFile(path) {
317
+ export function environmentFile(path) {
261
318
  const values = {};
262
319
  for (const line of readFileSync(path, 'utf8').split('\n')) {
263
320
  const at = line.indexOf('=');
@@ -295,6 +352,18 @@ export function keepFailureEvidence(name, evidence, env) {
295
352
  return `\n(the whole ${name} could not be kept: ${errorMessage(error)})`;
296
353
  }
297
354
  }
355
+ /** A package script's own path: each `node_modules/.bin` from the working directory up to the application's root. */
356
+ export function packageScriptPath(cwd, appRoot) {
357
+ const found = [];
358
+ for (let dir = cwd;; dir = dirname(dir)) {
359
+ const bin = join(dir, 'node_modules', '.bin');
360
+ if (existsSync(bin))
361
+ found.push(bin);
362
+ if (dir === appRoot || dirname(dir) === dir || !dir.startsWith(appRoot))
363
+ break;
364
+ }
365
+ return found;
366
+ }
298
367
  export async function runDatabasePrepare(step, worldConfig, launch) {
299
368
  const env = launch.env ?? process.env;
300
369
  const appRoot = env.VOLTER_WORLD_APP_ROOT;
@@ -305,13 +374,27 @@ export async function runDatabasePrepare(step, worldConfig, launch) {
305
374
  const { PATH: _imagePath, ...imageValues } = imageEnv;
306
375
  const worldValues = assertWorldConfig(JSON.parse(readFileSync(worldConfig, 'utf8')), worldConfig).env ?? {};
307
376
  const [program, ...words] = step.command;
308
- const migration = spawn(program, words, { windowsHide: true, cwd, env: { ...env, ...imageValues, ...worldValues }, stdio: ['ignore', 'pipe', 'pipe'] });
377
+ // THE STEP'S PROGRAM IS FOUND WHERE THE APPLICATION'S OWN START FINDS IT. A start written `yarn prisma:deploy`
378
+ // runs `prisma migrate deploy` with the package manager's path: each `node_modules/.bin` from the working
379
+ // directory up to the application's root, before PATH. The step is that script's command, so it is given that
380
+ // path; declared as a bare `prisma` and looked for on this process's PATH alone, it was not found, and a spawn
381
+ // that never started was reported as "exited null" with nothing else (linkwarden).
382
+ const scriptPath = packageScriptPath(cwd, appRoot);
383
+ const stepEnv = { ...env, ...imageValues, ...worldValues };
384
+ stepEnv.PATH = [...scriptPath, stepEnv.PATH ?? ''].filter(Boolean).join(delimiter);
385
+ const migration = spawn(program, words, { windowsHide: true, cwd, env: stepEnv, stdio: ['ignore', 'pipe', 'pipe'] });
309
386
  let output = '';
387
+ // A spawn that fails has no exit: what it met is kept and said, with where the program was looked for.
388
+ let notStarted;
389
+ migration.once('error', (error) => { notStarted = error; });
310
390
  migration.stdout.on('data', (chunk) => { output += chunk.toString('utf8'); });
311
391
  migration.stderr.on('data', (chunk) => { output += chunk.toString('utf8'); });
312
- const { code } = await commandLifetime(migration, launch.signal).finish();
392
+ const { code, signal: endedBy } = await commandLifetime(migration, launch.signal).finish();
393
+ if (notStarted) {
394
+ throw new Error(`the World's database prepare step (${step.command.join(' ')}) was NOT STARTED: ${notStarted.code ?? ''} ${notStarted.message}. Its program is ${program}; its working directory ${cwd} ${existsSync(cwd) ? 'is there' : 'IS NOT THERE (a missing directory is reported by the system as the program not found)'}; it was looked for in ${scriptPath.length ? scriptPath.join(', ') : 'no node_modules/.bin between that directory and the application\'s root'}, then on PATH (${(env.PATH ?? '').split(delimiter).filter(Boolean).length} entries)`);
395
+ }
313
396
  if (code !== 0)
314
- throw new Error(`the World's database prepare step (${step.command.join(' ')}) exited ${code ?? 'null'}\n${output.trim().split('\n').slice(-40).join('\n')}${keepFailureEvidence('database-prepare-step-output', output, env)}`);
397
+ throw new Error(`the World's database prepare step (${step.command.join(' ')}) ${code === null ? `was ended by ${endedBy ?? 'no exit status and no signal'}` : `exited ${code}`}\n${output.trim().split('\n').slice(-40).join('\n')}${keepFailureEvidence('database-prepare-step-output', output, env)}`);
315
398
  launch.note?.(`database prepare step: ${step.command.join(' ')}\n${output.trim()}`);
316
399
  return { command: [...step.command], inputs: migrationInputDigests(cwd, step.inputs) };
317
400
  }
@@ -324,14 +407,16 @@ async function migratePreparedCluster(service, directory, step, worldConfig, lau
324
407
  const pidFile = join(dirname(directory), 'pglite-prepare.pid');
325
408
  const boot = randomUUID();
326
409
  rmSync(listeningReceiptPath(pidFile), { force: true });
327
- const [command, ...args] = [...hostArgv(service, directory), '--prepared', '--listening-file', listeningReceiptPath(pidFile), '--boot', boot];
410
+ // --record-extensions: the step's host writes the extensions the migrations left beside the cluster, which a prepared
411
+ // open then loads alone (pglite-host.mjs DECLARED_EXTENSIONS, S2 R66b).
412
+ const [command, ...args] = [...hostArgv(service, directory), '--prepared', '--record-extensions', '--listening-file', listeningReceiptPath(pidFile), '--boot', boot];
328
413
  let hostOutput = '';
329
414
  const host = spawn(command, args, { windowsHide: true,
330
415
  env: { ...env, VOLTER_WORLD_PGLITE_USER: service.postgres.user, VOLTER_WORLD_PGLITE_DATABASE: service.postgres.database, VOLTER_WORLD_PGLITE_PASSWORD: service.postgres.password },
331
416
  stdio: ['ignore', 'pipe', 'pipe'] });
332
417
  host.stdout.on('data', (chunk) => { hostOutput += chunk.toString('utf8'); });
333
418
  host.stderr.on('data', (chunk) => { hostOutput += chunk.toString('utf8'); });
334
- const hostLifetime = commandLifetime(host, launch.signal);
419
+ const hostLifetime = commandLifetime(host, launch.signal, false, READY_TIMEOUT_MS);
335
420
  try {
336
421
  const deadline = Date.now() + READY_TIMEOUT_MS;
337
422
  while (!(host.pid && alive(host.pid) && await hostServes(service, pidFile, host.pid, boot))) {
@@ -378,6 +463,10 @@ export async function pglitePrepare(service, worldConfig, launch = {}) {
378
463
  // The app's own migration, where the World declares one: a cluster it did not complete is never stamped.
379
464
  const step = databasePrepareOf(worldConfig);
380
465
  let migration;
466
+ if (step?.with === 'world') {
467
+ rmSync(directory, { recursive: true, force: true });
468
+ throw new Error('the World\'s database prepare step is declared with: "world", which is prepared on the native PostgreSQL backing only; this preparation is on PGlite (no PostgreSQL programs on PATH or in VOLTER_WORLD_POSTGRES_BIN, or VOLTER_WORLD_INFRA_BACKING=pglite)');
469
+ }
381
470
  if (step) {
382
471
  try {
383
472
  migration = await migratePreparedCluster(service, directory, step, worldConfig, launch);
@@ -433,7 +522,8 @@ export function expectedMigration(worldConfig, env, directory) {
433
522
  inputs = migrationInputDigests(join(env.VOLTER_WORLD_APP_ROOT, step.cwd), step.inputs);
434
523
  if (!inputs)
435
524
  return { note: `prepared cluster ${directory} not taken: it is migrated, and neither the app's recorded digests nor its tree were given to compare (VOLTER_WORLD_APP_DIGESTS, VOLTER_WORLD_APP_ROOT)` };
436
- return { migration: { command: [...step.command], inputs: Object.fromEntries(step.inputs.map((input) => [input, inputs[input] ?? ''])) } };
525
+ return { migration: { command: [...step.command], inputs: Object.fromEntries(step.inputs.map((input) => [input, inputs[input] ?? ''])),
526
+ ...(step.with === 'world' ? { world: preparedWorldServices(worldConfig) } : {}) } };
437
527
  }
438
528
  /** Beside a service's data: the migration its live cluster carries, written when a migrated cluster is placed. */
439
529
  const migrationRecord = (serviceData) => `${serviceData}.migration.json`;
@@ -481,7 +571,7 @@ export const DEVELOPMENT_MIGRATION_NOTICE = 'development World: its database mig
481
571
  * is exactly `expected`; a mismatch is named, never taken. A migrated cluster's `migration` is recorded beside the data
482
572
  * it becomes (liveDatabaseMigration).
483
573
  */
484
- export function takePreparedCluster(prepared, expected, serviceData, mismatch, migration) {
574
+ export function takePreparedCluster(prepared, expected, serviceData, mismatch, migration, copy = (from, to) => cpSync(from, to, { recursive: true })) {
485
575
  const { directory, stamp } = prepared;
486
576
  if (existsSync(join(serviceData, 'PG_VERSION')))
487
577
  return { placed: false };
@@ -497,17 +587,32 @@ export function takePreparedCluster(prepared, expected, serviceData, mismatch, m
497
587
  }
498
588
  mkdirSync(dirname(serviceData), { recursive: true });
499
589
  // A rename moves the cluster without writing its files; across file systems it is copied.
590
+ // A caller may copy its own way (the native backing copies as the server's owner, so its cluster is walked once).
591
+ let copied = false;
500
592
  try {
501
593
  renameSync(directory, serviceData);
502
594
  }
503
595
  catch (error) {
504
596
  if (error.code !== 'EXDEV')
505
597
  throw error;
506
- cpSync(directory, serviceData, { recursive: true });
598
+ // Copied beside its place and moved in whole: a copy cut short (ENOSPC, a crash) never leaves a part of a cluster
599
+ // where the next up would refuse it, or, holding PG_VERSION, take it as placed and boot it truncated. A sibling an
600
+ // earlier start left behind is removed first.
601
+ const placing = `${serviceData}.placing`;
602
+ rmSync(placing, { recursive: true, force: true });
603
+ try {
604
+ copy(directory, placing);
605
+ renameSync(placing, serviceData);
606
+ }
607
+ catch (failure) {
608
+ rmSync(placing, { recursive: true, force: true });
609
+ throw failure;
610
+ }
611
+ copied = true;
507
612
  }
508
613
  if (migration)
509
614
  writeFileSync(migrationRecord(serviceData), `${JSON.stringify(migration)}\n`);
510
- return { placed: true, note: `prepared cluster ${directory} placed at ${serviceData}` };
615
+ return { placed: true, copied, note: `prepared cluster ${directory} placed at ${serviceData}${copied ? ' (copied: another file system)' : ''}` };
511
616
  }
512
617
  export async function pgliteUp(services, dataDir, launch = {}) {
513
618
  const env = launch.env ?? process.env;
@@ -544,10 +649,26 @@ export async function pgliteUp(services, dataDir, launch = {}) {
544
649
  appendFileSync(log, `${placed.note}\n`);
545
650
  launch.note?.(placed.note);
546
651
  }
652
+ const identity = service.kind === 'postgres' ? preparedStamp(service) : undefined;
653
+ const keptIdentity = identity !== undefined && hasDeclaredDatabase(serviceData, identity);
654
+ const direct = service.kind === 'postgres' && (placed.placed || keptIdentity);
655
+ if (identity !== undefined) {
656
+ // Maintenance may rename a legacy database. Its old identity must not survive a partial initialization.
657
+ if (!direct)
658
+ rmSync(declaredDatabaseReceipt(serviceData), { force: true });
659
+ const note = `${name} opening ${direct ? 'declared database directly' : 'template1 for database identity setup'} (${placed.placed ? 'placed prepared identity' : keptIdentity ? 'kept declared identity' : 'identity not established'})`;
660
+ appendFileSync(log, `${note}\n`);
661
+ launch.note?.(note);
662
+ }
547
663
  // This boot's token, which the host writes into its listener receipt; an earlier receipt is never this one's.
548
664
  const boot = randomUUID();
549
665
  rmSync(listeningReceiptPath(pidFile), { force: true });
550
- const [command, ...args] = [...(placed.placed ? [...argv, '--prepared'] : argv), ...(service.kind === 'postgres' ? ['--listening-file', listeningReceiptPath(pidFile), '--boot', boot] : [])];
666
+ // The declared migrations' extensions alone, only for a cluster this boot placed whose live data carries the image's
667
+ // migration (S2 R66b, review): kept data, data a newer image migrates, or a cluster opened another way loads every
668
+ // extension, and the host drops the record.
669
+ const declaredOnly = service.kind === 'postgres' && placed.placed && env.VOLTER_WORLD_CONFIG !== undefined
670
+ && liveDatabaseMigration(env.VOLTER_WORLD_CONFIG, env, serviceData) !== undefined;
671
+ const [command, ...args] = [...(direct ? [...argv, '--prepared'] : argv), ...(declaredOnly ? ['--declared-extensions'] : []), ...(service.kind === 'postgres' ? ['--listening-file', listeningReceiptPath(pidFile), '--boot', boot] : [])];
551
672
  // a real fd, not a pipe: the host must outlive this process untethered
552
673
  const logFd = openSync(log, 'a');
553
674
  const child = spawn(command, args, {
@@ -558,7 +679,7 @@ export async function pgliteUp(services, dataDir, launch = {}) {
558
679
  stdio: ['ignore', logFd, logFd],
559
680
  });
560
681
  // Retain the actual child BEFORE descriptor close or either publication can fail.
561
- const lifetime = commandLifetime(child, launch.signal);
682
+ const lifetime = commandLifetime(child, launch.signal, false, READY_TIMEOUT_MS);
562
683
  const host = { child, lifetime, closed: false };
563
684
  child.once('close', () => { host.closed = true; });
564
685
  hosts.set(pidFile, host);
@@ -591,7 +712,14 @@ export async function pgliteUp(services, dataDir, launch = {}) {
591
712
  launch.signal?.throwIfAborted();
592
713
  await sleep(25);
593
714
  }
594
- launch.note?.(`${name} serving on 127.0.0.1:${service.hostPort}`);
715
+ // Publish atomically only after this boot's own listener and protocol answer proved the database open.
716
+ // Kept opens read their identity once and need no extra write or fsync.
717
+ if (identity !== undefined && !keptIdentity)
718
+ publishProcessReceipt(declaredDatabaseReceipt(serviceData), identity);
719
+ // The host's own open split (pglite-host.mjs, THE OPEN'S SPLIT), from its receipt: said where a run's console
720
+ // sees it, as the host's stdout goes to its log alone (S2 R66).
721
+ const openSplit = service.kind === 'postgres' ? listeningReceipt(listeningReceiptPath(pidFile))?.openSplit : undefined;
722
+ launch.note?.(`${name} serving on 127.0.0.1:${service.hostPort}${openSplit ? ` (open split: ${openSplit})` : ''}`);
595
723
  if (service.kind === 'mongodb')
596
724
  await mongodbReady(service, launch, deadline);
597
725
  child.unref();
@@ -662,15 +790,21 @@ async function stopHost(pidFile) {
662
790
  await verifySpawnIdentity(ownership.identity);
663
791
  else
664
792
  throw new AggregateError(ownership.errors, 'Managed host spawn identity missing or unreadable; ownership retained');
665
- // Reuse commandLifetime's existing grace and confirmation budgets; no new limit.
666
- for (const [signal, limit] of [['SIGTERM', COMMAND_GRACE_MS], ['SIGKILL', COMMAND_CONFIRM_MS]]) {
793
+ // A STORE IS AWAITED AT ITS STOP on the lifecycle's own budget, not a command's grace: between the signal and
794
+ // its exit it writes its state down, and a SIGKILL in the middle of that leaves a root nothing may take.
795
+ // Its stop, its exit and the time between are events in the World's log; a kill is said as a kill.
796
+ const stopAsked = Date.now();
797
+ for (const [signal, limit] of [['SIGTERM', READY_TIMEOUT_MS], ['SIGKILL', COMMAND_CONFIRM_MS]]) {
667
798
  if (!live())
668
799
  break;
669
- await signalOwnedProcess(ownership.identity, signal, true);
800
+ worldEvent({ event: signal === 'SIGTERM' ? 'world.service.stop' : 'world.service.killed', service: 'managed host', by: 'down', signal, to: 'group', pid, ...(signal === 'SIGKILL' ? { afterMs: Date.now() - stopAsked, doing: 'its stop had not ended within the lifecycle budget' } : {}) });
801
+ await signalOwnedProcess(ownership.identity, signal, true, 'down');
670
802
  const deadline = Date.now() + limit;
671
803
  while (live() && Date.now() < deadline)
672
804
  await sleep(20);
673
805
  }
806
+ if (!live())
807
+ worldEvent({ event: 'world.service.exited', service: 'managed host', by: 'down', pid, shutdownMs: Date.now() - stopAsked });
674
808
  if (live())
675
809
  throw new Error(`managed infrastructure group ${pid} did not stop; retained ${pidFile}`);
676
810
  }