@volter/world-runtime 3.0.163 → 3.0.165

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/src/ancestry.js +2 -2
  2. package/dist/src/app-url.d.ts +5 -1
  3. package/dist/src/app-url.js +14 -1
  4. package/dist/src/branch.js +28 -8
  5. package/dist/src/browser-files.d.ts +5 -0
  6. package/dist/src/browser-files.js +14 -0
  7. package/dist/src/browsers.d.ts +2 -3
  8. package/dist/src/browsers.js +4 -4
  9. package/dist/src/catalog.d.ts +2 -1
  10. package/dist/src/catalog.js +9 -3
  11. package/dist/src/changeset.js +1 -1
  12. package/dist/src/cli-commands.js +71 -18
  13. package/dist/src/cli-load-clock.d.ts +1 -0
  14. package/dist/src/cli-load-clock.js +2 -0
  15. package/dist/src/cli.js +5 -1
  16. package/dist/src/clickhouse-backing.d.ts +2 -0
  17. package/dist/src/clickhouse-backing.js +145 -0
  18. package/dist/src/command-lifetime.d.mts +1 -1
  19. package/dist/src/command-lifetime.mjs +5 -2
  20. package/dist/src/configs.js +1 -1
  21. package/dist/src/consumers.js +1 -1
  22. package/dist/src/covers.d.ts +4 -2
  23. package/dist/src/covers.js +8 -4
  24. package/dist/src/go-mysql-binding.d.ts +19 -0
  25. package/dist/src/go-mysql-binding.js +51 -0
  26. package/dist/src/guest-processes.d.mts +1 -1
  27. package/dist/src/guest-processes.mjs +22 -14
  28. package/dist/src/host-cli.js +8 -1
  29. package/dist/src/host-ready.d.ts +16 -2
  30. package/dist/src/host-ready.js +66 -39
  31. package/dist/src/host-worker.js +7 -1
  32. package/dist/src/host.d.ts +26 -0
  33. package/dist/src/host.js +93 -3
  34. package/dist/src/infra-definition.d.ts +35 -0
  35. package/dist/src/infra-definition.js +146 -0
  36. package/dist/src/infra.d.ts +3 -0
  37. package/dist/src/infra.js +375 -105
  38. package/dist/src/init.d.ts +46 -0
  39. package/dist/src/init.js +446 -19
  40. package/dist/src/lifecycle-record.d.ts +5 -1
  41. package/dist/src/lifecycle-record.js +27 -4
  42. package/dist/src/local-branches.js +1 -1
  43. package/dist/src/mariadb-bootstrap.d.ts +32 -0
  44. package/dist/src/mariadb-bootstrap.js +90 -0
  45. package/dist/src/mariadb-supplier.d.ts +23 -0
  46. package/dist/src/mariadb-supplier.js +215 -0
  47. package/dist/src/mongodb-backing.d.ts +4 -0
  48. package/dist/src/mongodb-backing.js +71 -1
  49. package/dist/src/native-mariadb-backing.d.ts +13 -0
  50. package/dist/src/native-mariadb-backing.js +512 -0
  51. package/dist/src/native-postgres-backing.d.ts +4 -10
  52. package/dist/src/native-postgres-backing.js +428 -43
  53. package/dist/src/native-server-inputs.d.ts +16 -0
  54. package/dist/src/native-server-inputs.js +204 -0
  55. package/dist/src/origin.js +3 -3
  56. package/dist/src/original-inputs.d.ts +14 -0
  57. package/dist/src/original-inputs.js +84 -0
  58. package/dist/src/pack-facts.d.ts +9 -1
  59. package/dist/src/pglite-backing.d.ts +41 -17
  60. package/dist/src/pglite-backing.js +213 -79
  61. package/dist/src/pglite-host.mjs +482 -10
  62. package/dist/src/process-groups.d.mts +6 -1
  63. package/dist/src/process-groups.d.ts +2 -1
  64. package/dist/src/process-groups.js +1 -1
  65. package/dist/src/process-groups.mjs +82 -15
  66. package/dist/src/process-signal.d.mts +2 -2
  67. package/dist/src/process-signal.mjs +29 -9
  68. package/dist/src/project-inspect.d.ts +12 -0
  69. package/dist/src/project-inspect.js +41 -4
  70. package/dist/src/proxy-env.d.ts +34 -0
  71. package/dist/src/proxy-env.js +114 -0
  72. package/dist/src/redirect-proxy.d.ts +1 -31
  73. package/dist/src/redirect-proxy.js +3 -112
  74. package/dist/src/redis-backing.d.ts +6 -4
  75. package/dist/src/redis-backing.js +20 -168
  76. package/dist/src/runtime.d.ts +1 -0
  77. package/dist/src/runtime.js +503 -218
  78. package/dist/src/schema.d.ts +1 -0
  79. package/dist/src/schema.js +5 -3
  80. package/dist/src/served-world.js +3 -3
  81. package/dist/src/twin-backing.d.ts +47 -0
  82. package/dist/src/twin-backing.js +343 -0
  83. package/dist/src/up-cli.js +56 -3
  84. package/package.json +3 -3
  85. package/src/ancestry.ts +2 -2
  86. package/src/app-url.ts +9 -2
  87. package/src/branch.ts +26 -8
  88. package/src/browser-files.ts +15 -0
  89. package/src/browsers.ts +4 -4
  90. package/src/catalog.ts +9 -4
  91. package/src/changeset.ts +1 -1
  92. package/src/cli-commands.ts +85 -18
  93. package/src/cli-load-clock.ts +2 -0
  94. package/src/cli.ts +5 -1
  95. package/src/clickhouse-backing.ts +70 -0
  96. package/src/command-lifetime.d.mts +1 -1
  97. package/src/command-lifetime.mjs +5 -2
  98. package/src/configs.ts +1 -1
  99. package/src/consumers.ts +1 -1
  100. package/src/covers.ts +9 -4
  101. package/src/go-mysql-binding.ts +49 -0
  102. package/src/guest-processes.d.mts +1 -1
  103. package/src/guest-processes.mjs +22 -14
  104. package/src/host-cli.ts +7 -1
  105. package/src/host-ready.ts +62 -31
  106. package/src/host-worker.ts +6 -2
  107. package/src/host.ts +88 -6
  108. package/src/infra-definition.ts +135 -0
  109. package/src/infra.ts +250 -45
  110. package/src/init.ts +417 -18
  111. package/src/lifecycle-record.ts +18 -4
  112. package/src/local-branches.ts +1 -1
  113. package/src/mariadb-bootstrap.ts +107 -0
  114. package/src/mariadb-supplier.ts +199 -0
  115. package/src/mongodb-backing.ts +59 -1
  116. package/src/native-mariadb-backing.ts +418 -0
  117. package/src/native-postgres-backing.ts +353 -41
  118. package/src/native-server-inputs.ts +169 -0
  119. package/src/origin.ts +3 -3
  120. package/src/original-inputs.ts +72 -0
  121. package/src/pack-facts.ts +1 -1
  122. package/src/pglite-backing.ts +192 -80
  123. package/src/pglite-host.mjs +482 -10
  124. package/src/process-groups.d.mts +6 -1
  125. package/src/process-groups.mjs +82 -15
  126. package/src/process-groups.ts +2 -1
  127. package/src/process-signal.d.mts +2 -2
  128. package/src/process-signal.mjs +29 -9
  129. package/src/project-inspect.ts +37 -4
  130. package/src/proxy-env.ts +128 -0
  131. package/src/redirect-proxy.ts +3 -125
  132. package/src/redis-backing.ts +19 -136
  133. package/src/runtime.ts +342 -107
  134. package/src/schema.ts +5 -4
  135. package/src/served-world.ts +3 -3
  136. package/src/twin-backing.ts +300 -0
  137. package/src/up-cli.ts +52 -3
@@ -2,7 +2,7 @@
2
2
  // belong to the infrastructure service; a recipe never operates postgres itself.
3
3
  import { spawn } from 'node:child_process';
4
4
  import { createHash } from 'node:crypto';
5
- import { accessSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
+ import { accessSync, chmodSync, chownSync, copyFileSync, lchownSync, lstatSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
6
6
  import { connect } from 'node:net';
7
7
  import { runtimeManifest } from "./package-manifest.js";
8
8
  import { delimiter, dirname, join, resolve } from 'node:path';
@@ -10,8 +10,94 @@ import { commandLifetime, COMMAND_GRACE_MS, CommandRetirementError } from "./com
10
10
  import { survivingOwnedGroups } from "./process-groups.js";
11
11
  import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess } from "./process-receipt.js";
12
12
  import { signalChild, childIdentity } from './process-signal.mjs';
13
+ import { worldEvent } from './process-groups.mjs';
13
14
  import { errorMessage } from "./error-message.js";
14
- import { PREPARED_DIRECTORY, databasePrepareOf, expectedMigration, forgetStaleMigration, keepFailureEvidence, runDatabasePrepare, takePreparedCluster } from "./pglite-backing.js";
15
+ import { PREPARED_DIRECTORY, PREPARE_STATE_DIRECTORY, packageScriptPath, databasePrepareOf, environmentFile, expectedMigration, forgetStaleMigration, keepFailureEvidence, migrationInputDigests, preparedStore, preparedStoreStamp, preparedWorldServices, runDatabasePrepare, takePreparedCluster } from "./pglite-backing.js";
16
+ /** OS identity, distinct from PostgreSQL's configured SQL role. No in-process privilege mutation. */
17
+ function postgresRunAs() {
18
+ const uid = process.getuid?.();
19
+ const gid = process.getgid?.();
20
+ if (uid === undefined || gid === undefined)
21
+ throw new Error('native PostgreSQL requires a numeric OS process identity');
22
+ if (uid !== 0)
23
+ return { uid, gid };
24
+ const accounts = readFileSync('/etc/passwd', 'utf8').split('\n').map(line => line.split(':'));
25
+ for (const name of ['postgres', 'nobody']) {
26
+ const account = accounts.find(fields => fields[0] === name && /^\d+$/.test(fields[2] ?? '') && /^\d+$/.test(fields[3] ?? '')
27
+ && Number(fields[2]) > 0 && Number(fields[2]) < 0xffffffff && Number(fields[3]) > 0 && Number(fields[3]) < 0xffffffff);
28
+ if (account)
29
+ return { uid: Number(account[2]), gid: Number(account[3]) };
30
+ }
31
+ throw new Error('native PostgreSQL started by root requires an installed non-root postgres or nobody account');
32
+ }
33
+ /**
34
+ * The prepared cluster copied as the server's own (S2 R15): each directory, file and link made, its mode taken from
35
+ * the source as cpSync takes it, and given to the server's uid and gid as it is made. A cpSync and then
36
+ * ownPostgresCluster's walk asked the tree for every one of the cluster's files twice more (Cal in a tab: the
37
+ * placing step 170 ms, each ask a kernel request from up's process).
38
+ */
39
+ function copyClusterOwned(runAs) {
40
+ const copy = (from, to) => {
41
+ const stat = lstatSync(from);
42
+ if (stat.isSymbolicLink()) {
43
+ symlinkSync(readlinkSync(from), to);
44
+ lchownSync(to, runAs.uid, runAs.gid);
45
+ return;
46
+ }
47
+ if (stat.isDirectory()) {
48
+ mkdirSync(to);
49
+ for (const name of readdirSync(from))
50
+ copy(join(from, name), join(to, name));
51
+ }
52
+ else if (stat.isFile())
53
+ copyFileSync(from, to);
54
+ else
55
+ throw new Error(`native PostgreSQL's prepared cluster holds a ${stat.isFIFO() ? 'FIFO' : 'special file'}, which is not copied: ${from}`);
56
+ chmodSync(to, stat.mode & 0o7777);
57
+ chownSync(to, runAs.uid, runAs.gid);
58
+ };
59
+ return copy;
60
+ }
61
+ /** Assign only the stopped World's cluster, including a cluster placed from preparation. Never follow links. */
62
+ function ownPostgresCluster(data, runAs) {
63
+ // What the walk did, said by the start's step (S2 R71): every entry is looked at, and only another owner's changed.
64
+ const counts = { walked: 0, changed: 0 };
65
+ const visit = (path) => {
66
+ const stat = lstatSync(path);
67
+ counts.walked += 1;
68
+ if (stat.uid !== runAs.uid || stat.gid !== runAs.gid) {
69
+ counts.changed += 1;
70
+ if (stat.isSymbolicLink())
71
+ lchownSync(path, runAs.uid, runAs.gid);
72
+ else
73
+ chownSync(path, runAs.uid, runAs.gid);
74
+ }
75
+ if (stat.isDirectory())
76
+ for (const name of readdirSync(path))
77
+ visit(join(path, name));
78
+ };
79
+ visit(data);
80
+ return counts;
81
+ }
82
+ /** The service may traverse its own World bookkeeping directories, without reading their private receipts. */
83
+ function postgresDirectorySearch(root, env, runAs) {
84
+ const directories = [root];
85
+ // The live lifecycle creates instance/data as 0700. Only its recorded instance is granted search;
86
+ // preparation has a different root and never changes the application's directory above it.
87
+ if (env.VOLTER_WORLD_INSTANCE && env.VOLTER_WORLD_DATA
88
+ && resolve(env.VOLTER_WORLD_DATA) === resolve(root)
89
+ && dirname(resolve(env.VOLTER_WORLD_INSTANCE)) === dirname(resolve(root)))
90
+ directories.push(dirname(resolve(root)));
91
+ for (const directory of directories) {
92
+ const stat = lstatSync(directory);
93
+ if (!stat.isDirectory())
94
+ throw new Error(`native PostgreSQL directory is not a directory: ${directory}`);
95
+ if ((stat.uid === runAs.uid && (stat.mode & 0o100)) || (stat.gid === runAs.gid && (stat.mode & 0o010)) || (stat.mode & 0o001))
96
+ continue;
97
+ chownSync(directory, stat.uid, runAs.gid);
98
+ chmodSync(directory, (stat.mode & 0o7777) | 0o010);
99
+ }
100
+ }
15
101
  const starts = new Map();
16
102
  const wait = (ms) => new Promise(done => setTimeout(done, ms));
17
103
  const recordPath = (root) => join(root, 'native-postgres.owner.json');
@@ -43,11 +129,73 @@ function ownership(root) {
43
129
  }
44
130
  return { present: records.some(row => row.present) || spawn.present, held: values.find(row => row.pid === spawn.value?.pid) ?? values[0], identity: spawn.value, values, errors };
45
131
  }
132
+ /**
133
+ * Whether postmaster.pid names the postmaster this lifecycle holds. The postmaster CREATES that file and then writes
134
+ * it, and removes it as it exits: a reader meets it empty, part written, or gone. Those are not a cluster owned by
135
+ * another; they are a file not written yet (false: ask again). A mismatch is four COMPLETE lines of which the pid,
136
+ * the data directory or the port differs, and it is refused with what was read beside what was held.
137
+ */
46
138
  function verifyOwner(held) {
47
- const lines = readFileSync(join(held.data, 'postmaster.pid'), 'utf8').split('\n');
139
+ let text;
140
+ try {
141
+ text = readFileSync(join(held.data, 'postmaster.pid'), 'utf8');
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ const lines = text.split('\n');
147
+ // Lines 1-4 are pid, data directory, start time, port; each is complete when a line follows it.
148
+ if (lines.length < 5 || lines.slice(0, 4).some((line) => line.trim() === ''))
149
+ return false;
48
150
  if (Number(lines[0]) !== held.pid || resolve(lines[1]) !== held.data || Number(lines[3]) !== held.port) {
49
- throw new Error('native postgres ownership does not match the cluster; teardown refused');
151
+ // A FILE LEFT BY A POSTMASTER THAT IS GONE IS STALE, NOT ANOTHER OWNER. After a kill or a crash the file stays,
152
+ // four complete lines naming a pid with no process; the postmaster just started reads it, finds that, and
153
+ // writes its own. Read in between, it names neither this postmaster nor a live one: not written yet. Only a
154
+ // file naming a LIVE process that is not this one, or this one with another directory or port, is a mismatch.
155
+ const named = Number(lines[0]);
156
+ if (named !== held.pid && Number.isInteger(named) && named > 0) {
157
+ let gone = false;
158
+ try {
159
+ process.kill(named, 0);
160
+ }
161
+ catch (error) {
162
+ gone = error.code === 'ESRCH';
163
+ }
164
+ if (gone)
165
+ return false;
166
+ }
167
+ throw new Error(`native postgres ownership does not match the cluster; teardown refused (postmaster.pid says pid ${lines[0]}, data ${lines[1]}, port ${lines[3]}; this lifecycle holds pid ${held.pid}, data ${held.data}, port ${held.port})`);
168
+ }
169
+ return true;
170
+ }
171
+ /**
172
+ * THE POSTMASTER'S STOP. PostgreSQL's contract for a stop is one signal to the postmaster (SIGINT, its fast
173
+ * shutdown; what pg_ctl sends): it ends its backends itself, writes the shutdown checkpoint and removes
174
+ * postmaster.pid. A signal to its GROUP reaches the checkpointer and the writers too, which are not the ones to
175
+ * tell. Between the signal and its exit the postmaster is writing the cluster down, and how long that takes is the
176
+ * size of what is dirty, so it is awaited on the lifecycle's own budget (READY_TIMEOUT_MS), never a command's
177
+ * grace: a SIGKILL in the middle of the checkpoint leaves a cluster with its pid file, which nothing takes. Only
178
+ * past that budget is the group killed, and that is said as a kill. The stop, the exit, and the time between are
179
+ * events in the World's log.
180
+ */
181
+ async function stopPostmaster(pid, deliver, by, data) {
182
+ if (!liveGroup(pid))
183
+ return;
184
+ const asked = Date.now();
185
+ worldEvent({ event: 'world.service.stop', service: 'postgres', by, signal: 'SIGINT', to: 'pid', pid });
186
+ await deliver('SIGINT', false);
187
+ const deadline = asked + READY_TIMEOUT_MS;
188
+ while (liveGroup(pid) && Date.now() < deadline)
189
+ await wait(100);
190
+ if (!liveGroup(pid)) {
191
+ worldEvent({ event: 'world.service.exited', service: 'postgres', by, pid, shutdownMs: Date.now() - asked, ...(data ? { pidFileLeft: existsSync(join(data, 'postmaster.pid')) } : {}) });
192
+ return;
50
193
  }
194
+ worldEvent({ event: 'world.service.killed', service: 'postgres', by, pid, signal: 'SIGKILL', to: 'group', afterMs: Date.now() - asked, doing: 'its shutdown had not ended within the lifecycle budget; the cluster is left with its pid file and is not taken' });
195
+ await deliver('SIGKILL', true);
196
+ const confirm = Date.now() + COMMAND_GRACE_MS;
197
+ while (liveGroup(pid) && Date.now() < confirm)
198
+ await wait(20);
51
199
  }
52
200
  const liveGroup = (pid) => pid > 0 && survivingOwnedGroups([pid]).length > 0;
53
201
  function removeReceipts(root) {
@@ -73,6 +221,12 @@ function removeReceipts(root) {
73
221
  if (errors.length)
74
222
  throw new AggregateError(errors, 'native postgres retired but receipt cleanup failed');
75
223
  }
224
+ async function retirePostmaster(identity, by, data) {
225
+ await stopPostmaster(identity.pid, (signal, group) => signalOwnedProcess(identity, signal, group, by), by, data);
226
+ if (liveGroup(identity.pid))
227
+ throw new Error(`native postgres group ${identity.pid} retirement unconfirmed; ownership retained`);
228
+ }
229
+ /** A helper this backing started (psql, createdb, initdb): it holds no state, and keeps a command's grace. */
76
230
  async function retireIdentity(identity) {
77
231
  const { pid } = identity;
78
232
  for (const signal of ['SIGINT', 'SIGKILL']) {
@@ -87,21 +241,18 @@ async function retireIdentity(identity) {
87
241
  throw new Error(`native postgres group ${pid} retirement unconfirmed; ownership retained`);
88
242
  }
89
243
  /** A retained child is ownership even before postmaster.pid or the ready receipt exists. */
90
- async function retireStarted(started) {
244
+ async function retireStarted(started, by = 'down (this process started it)') {
91
245
  const { child, owned } = started;
92
246
  child.ref();
93
- for (const signal of ['SIGINT', 'SIGKILL']) {
247
+ if (owned.pid > 0) {
248
+ await stopPostmaster(owned.pid, async (signal, group) => signalChild(child, signal, group, by)
249
+ || (liveGroup(owned.pid) ? signalOwnedProcess(started.identity ?? await started.birth, signal, group, by) : false), by, owned.data);
250
+ }
251
+ const deadline = Date.now() + COMMAND_GRACE_MS;
252
+ while (Date.now() < deadline) {
94
253
  if (started.closed && !liveGroup(owned.pid))
95
254
  return;
96
- if (owned.pid > 0 && !signalChild(child, signal, true) && liveGroup(owned.pid)) {
97
- await signalOwnedProcess(started.identity ?? await started.birth, signal, true);
98
- }
99
- const deadline = Date.now() + COMMAND_GRACE_MS;
100
- while (Date.now() < deadline) {
101
- if (started.closed && !liveGroup(owned.pid))
102
- return;
103
- await wait(20);
104
- }
255
+ await wait(20);
105
256
  }
106
257
  throw new Error(`native postgres child ${owned.pid} retirement unconfirmed; ownership retained`);
107
258
  }
@@ -144,12 +295,12 @@ function logTail(log) {
144
295
  }
145
296
  }
146
297
  /** Every helper joins exit and its private group, including spawn errors and abort. */
147
- async function tool(command, args, env, deadline, root, signal) {
298
+ async function tool(command, args, env, deadline, root, runAs, signal) {
148
299
  signal?.throwIfAborted();
149
300
  const remaining = deadline - Date.now();
150
301
  if (remaining <= 0)
151
302
  throw new Error(`native postgres readiness deadline elapsed before ${command}`);
152
- const child = spawn(command, args, { env, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
303
+ const child = spawn(command, args, { env, ...runAs, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
153
304
  const lifetime = commandLifetime(child, signal);
154
305
  let output = '';
155
306
  let errorOutput = '';
@@ -265,6 +416,17 @@ export function nativePostgresTools(env) {
265
416
  }
266
417
  return undefined;
267
418
  }
419
+ /**
420
+ * How a cluster left running is made durable before its recovery: one syncfs of its file system, where PostgreSQL's
421
+ * default opens and fsyncs every file under the data directory. A World's postmaster is not stopped when its tab
422
+ * closes, so a returning visit recovers: the Umami f7b17202 pair's second visit spent 652 ms in the startup process
423
+ * (3,078 opens, 1,496 fsyncs, 244 directory reads), where the first's took 77 ms, and was ready at check 8, not 5
424
+ * (S2 L1). syncfs is Linux's (PostgreSQL builds it where the C library has it: glibc, and the musl the tab's postgres
425
+ * is built with); macOS has none, and an option its build lacks is refused at start, so only Linux asks for it.
426
+ */
427
+ function recoverySync() {
428
+ return process.platform === 'linux' ? ['-c', 'recovery_init_sync_method=syncfs'] : [];
429
+ }
268
430
  /** initdb's arguments after `-D`, the same at up and at prepare, and so in the stamp. */
269
431
  function initdbArguments(user) {
270
432
  return [`--username=${user}`, '--auth=trust', '--no-locale', '--encoding=UTF8'];
@@ -329,6 +491,171 @@ function nativeStampMatches(text, expected) {
329
491
  * step runs against that postmaster before its stop, and a step that fails leaves no cluster. The image build runs it with the programs the tab
330
492
  * runs, under the tab's kernel, so the cluster is the one a first boot there would write.
331
493
  */
494
+ /** Every log file under a state directory, as one text with each file's path before its lines; empty when none. */
495
+ function worldLogs(state) {
496
+ const out = [];
497
+ const walk = (directory) => {
498
+ let names;
499
+ try {
500
+ names = readdirSync(directory).sort();
501
+ }
502
+ catch {
503
+ return;
504
+ }
505
+ for (const name of names) {
506
+ const path = join(directory, name);
507
+ let stats;
508
+ try {
509
+ stats = statSync(path);
510
+ }
511
+ catch {
512
+ continue;
513
+ }
514
+ if (stats.isDirectory()) {
515
+ if (name !== 'native-postgres-data' && name !== 'node_modules')
516
+ walk(path);
517
+ }
518
+ else if (stats.isFile() && /\.log$/.test(name) && stats.size <= 8 * 1024 * 1024) {
519
+ try {
520
+ out.push(`==> ${path} <==\n${readFileSync(path, 'utf8')}`);
521
+ }
522
+ catch { /* a log that cannot be read is left out */ }
523
+ }
524
+ }
525
+ };
526
+ walk(state);
527
+ return out.join('\n');
528
+ }
529
+ /**
530
+ * A managed store, other than the cluster, that keeps its bytes under the World's data and is staged with it: the
531
+ * tree of a service a twin serves, which twin-backing.ts names `<kind>-twin` (redis-twin).
532
+ */
533
+ const stagedStore = (name) => /^[a-z0-9]+-twin$/.test(name);
534
+ /** The state directory a preparation with the World up boots its disposable instance in, beside the World's own. */
535
+ /**
536
+ * A cluster migrated by a step that needs the World (`runtime.database.prepare.with: "world"`): a migration that
537
+ * talks to a service the World supplies (a cache it flushes, a queue it registers with, a vendor it reads) cannot
538
+ * run against a bare postmaster. The World is brought up whole, as `run` brings it up, in a state directory of its
539
+ * own beside the World's (`.volter-prepare`), with no prepared cluster to take, so its managed PostgreSQL is made
540
+ * new by initdb; the step is the World's foreground command, in the app's tree, with the image's environment under
541
+ * the World's own (the names its twins issued and its infrastructure discovered); the World then goes down, which
542
+ * stops the postmaster cleanly. The stopped cluster is moved to the World's prepared place and stamped with the
543
+ * step, its inputs' digests and the twins that were up. Each other managed store that keeps its bytes under the
544
+ * World's data (the tree of each service a twin serves, `<kind>-twin`: the Redis twin's root) is staged beside the cluster with the same migration in a
545
+ * stamp of its own, since what the step wrote there is the World's prepared state as much as its tables are.
546
+ * Everything else that instance wrote (a twin's own state, its logs) is named in the note and removed with the
547
+ * state directory. A step that fails leaves no cluster, keeps the step's whole output where a failed preparation
548
+ * keeps its evidence, and says so.
549
+ */
550
+ async function prepareWithWorld(step, definition, worldConfig, tools, env, launch) {
551
+ const appRoot = env.VOLTER_WORLD_APP_ROOT;
552
+ if (!appRoot)
553
+ throw new Error('the World declares runtime.database.prepare: VOLTER_WORLD_APP_ROOT must name the app image\'s root');
554
+ const root = dirname(worldConfig);
555
+ const { directory, stamp } = nativePreparedCluster(worldConfig);
556
+ const state = join(root, PREPARE_STATE_DIRECTORY);
557
+ rmSync(directory, { recursive: true, force: true });
558
+ rmSync(stamp, { force: true });
559
+ rmSync(state, { recursive: true, force: true });
560
+ // The other stores a step run with the World up may write, staged beside the cluster (architecture, "A step may
561
+ // need the World"): each by the name of its directory under the World's data.
562
+ const previouslyStaged = () => { try {
563
+ return readdirSync(dirname(directory)).filter(stagedStore);
564
+ }
565
+ catch {
566
+ return [];
567
+ } };
568
+ const unstage = () => { for (const name of previouslyStaged()) {
569
+ const store = preparedStore(worldConfig, name);
570
+ rmSync(store.directory, { recursive: true, force: true });
571
+ rmSync(store.stamp, { force: true });
572
+ } };
573
+ unstage();
574
+ const cwd = join(appRoot, step.cwd);
575
+ const { user, database } = settings(definition);
576
+ // The World's run reads this process's environment: the preparation's own (its PostgreSQL programs, the app's
577
+ // root), the image's values over it as for a step run alone, and the state directory. This process is the
578
+ // preparation and nothing else, so its environment is the preparation's to set.
579
+ const { PATH: _imagePath, ...imageValues } = env.VOLTER_WORLD_APP_ENV ? environmentFile(env.VOLTER_WORLD_APP_ENV) : {};
580
+ const before = { ...process.env };
581
+ for (const [name, value] of Object.entries({ ...env, ...imageValues, VOLTER_STATE_DIR: PREPARE_STATE_DIRECTORY }))
582
+ if (value !== undefined)
583
+ process.env[name] = value;
584
+ // The step is a package script's command here too, and is found where the application's own start finds it
585
+ // (pglite-backing.ts runDatabasePrepare): each node_modules/.bin from its working directory upward, before PATH.
586
+ process.env.PATH = [...packageScriptPath(cwd, appRoot), process.env.PATH ?? ''].filter(Boolean).join(delimiter);
587
+ delete process.env.VOLTER_WORLD_CONFIG;
588
+ const names = join(state, 'prepare.env');
589
+ let moved = false;
590
+ try {
591
+ const { runWorldOwned } = await import("./runtime.js");
592
+ let ran;
593
+ try {
594
+ ran = await runWorldOwned(worldConfig, step.command, { root, cwd, owner: 'database-prepare', envFile: names, forceEnvOutput: true }, launch.signal);
595
+ }
596
+ catch (error) {
597
+ throw new Error(`the World's database prepare step (${step.command.join(' ')}), run with the World up, did not complete: ${errorMessage(error)}`, { cause: error });
598
+ }
599
+ const log = ran.outcome.log, output = log && existsSync(log) ? readFileSync(log, 'utf8') : '';
600
+ if (ran.exitCode !== 0) {
601
+ throw new Error(`the World's database prepare step (${step.command.join(' ')}), run with the World up, exited ${ran.exitCode}\n${output.trim().split('\n').slice(-40).join('\n')}${keepFailureEvidence('database-prepare-step-output', output, env)}`);
602
+ }
603
+ const data = join(ran.instance.dirs.data, 'native-postgres-data');
604
+ if (!existsSync(join(data, 'PG_VERSION')))
605
+ throw new Error(`the World came up without a native PostgreSQL cluster at ${data}: a step declared with: "world" is prepared on the native backing only`);
606
+ if (existsSync(join(data, 'postmaster.pid')))
607
+ throw new Error('the World went down and left its postmaster\'s pid file: the cluster was not stopped cleanly and is not taken');
608
+ const given = existsSync(names) ? readFileSync(names, 'utf8').split('\n').map((line) => /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/.exec(line)?.[1]).filter((name) => name !== undefined) : [];
609
+ const world = preparedWorldServices(worldConfig);
610
+ mkdirSync(dirname(directory), { recursive: true });
611
+ renameSync(data, directory);
612
+ moved = true;
613
+ const migration = { command: [...step.command], inputs: migrationInputDigests(cwd, step.inputs), world };
614
+ const staged = [];
615
+ for (const name of readdirSync(ran.instance.dirs.data).filter(stagedStore).sort()) {
616
+ const from = join(ran.instance.dirs.data, name), store = preparedStore(worldConfig, name);
617
+ if (!statSync(from).isDirectory() || !readdirSync(from).length)
618
+ continue;
619
+ // The twin that served this root is gone when its pid file is: the World's down waits for it and removes the
620
+ // file (twin-backing.ts). One left behind is a twin that may still be writing, as a postmaster's is above.
621
+ const left = readdirSync(ran.instance.dirs.data).filter((entry) => entry.startsWith(`${name}-`) && entry.endsWith('.pid'));
622
+ if (left.length)
623
+ throw new Error(`the World went down and left ${left.join(', ')}: the ${name} was not stopped cleanly and its root is not staged`);
624
+ renameSync(from, store.directory);
625
+ writeFileSync(store.stamp, preparedStoreStamp(migration));
626
+ staged.push(name);
627
+ }
628
+ // Everything else the instance left under its data, by name: not staged, and gone with the state directory.
629
+ const notStaged = readdirSync(ran.instance.dirs.data).filter((name) => !/\.(?:log|pid|json)$/.test(name) && !/^(?:logs?|run)$/.test(name)).sort();
630
+ // The cluster's stamp is written LAST: a preparation killed before here leaves a cluster nothing takes, never a
631
+ // stamped cluster whose stores did not arrive.
632
+ writeFileSync(stamp, `${JSON.stringify(nativeStamp(tools, user, database, true, migration))}\n`);
633
+ launch.note?.(`prepared stores beside the cluster: ${staged.join(', ') || 'none'}${notStaged.length ? `; left under the instance's data and not staged: ${notStaged.join(', ')}` : ''}`);
634
+ launch.note?.(`database prepare step, with the World up (${Object.entries(world).map(([id, version]) => `${id}@${version}`).join(', ') || 'no twins'}; ${given.length} names given: ${given.join(', ')}): ${step.command.join(' ')}\n${output.trim()}`);
635
+ return directory;
636
+ }
637
+ catch (error) {
638
+ if (moved) {
639
+ rmSync(directory, { recursive: true, force: true });
640
+ rmSync(stamp, { force: true });
641
+ }
642
+ unstage();
643
+ // The instance's own logs go with the state directory below, and they are where a World that did not come up
644
+ // says why (the co-located host's output, a managed service's log, the lifecycle log). They are kept first,
645
+ // whole, where a failed preparation keeps its evidence, and the error names the file.
646
+ const logs = worldLogs(state);
647
+ if (logs && error instanceof Error)
648
+ error.message += keepFailureEvidence('database-prepare-world-logs', logs, env);
649
+ throw error;
650
+ }
651
+ finally {
652
+ rmSync(state, { recursive: true, force: true });
653
+ for (const name of Object.keys(process.env))
654
+ if (!(name in before))
655
+ delete process.env[name];
656
+ Object.assign(process.env, before);
657
+ }
658
+ }
332
659
  export async function nativePrepare(service, definition, worldConfig, launch = {}) {
333
660
  if (service.kind !== 'postgres')
334
661
  throw new Error('only a declared PostgreSQL is prepared');
@@ -338,6 +665,9 @@ export async function nativePrepare(service, definition, worldConfig, launch = {
338
665
  const tools = nativePostgresTools(env);
339
666
  if (!tools)
340
667
  throw new Error('native prepare requires the PostgreSQL programs: a postgres server on PATH, or VOLTER_WORLD_POSTGRES_BIN');
668
+ const withWorld = databasePrepareOf(worldConfig);
669
+ if (withWorld?.with === 'world')
670
+ return prepareWithWorld(withWorld, definition, worldConfig, tools, env, launch);
341
671
  const { directory, stamp } = nativePreparedCluster(worldConfig);
342
672
  const root = dirname(directory);
343
673
  mkdirSync(root, { recursive: true });
@@ -380,14 +710,7 @@ export async function nativePrepare(service, definition, worldConfig, launch = {
380
710
  }
381
711
  // The clean stop: SIGINT is the postmaster's fast shutdown, which writes its checkpoint and removes postmaster.pid.
382
712
  // Its wait is the lifecycle's own readiness budget, not a new limit; down then clears the receipts.
383
- const identity = readSpawnReceipt(recordPath(root)).value;
384
- if (identity) {
385
- await signalOwnedProcess(identity, 'SIGINT', true);
386
- const deadline = Date.now() + READY_TIMEOUT_MS;
387
- while (liveGroup(identity.pid) && Date.now() < deadline)
388
- await wait(100);
389
- }
390
- await nativePostgresDown(root);
713
+ await nativePostgresDown(root, 'prepare (the cluster is made; its clean stop)');
391
714
  if (stepFailure) {
392
715
  rmSync(directory, { recursive: true, force: true });
393
716
  throw failed(stepFailure.error);
@@ -400,10 +723,21 @@ export async function nativePrepare(service, definition, worldConfig, launch = {
400
723
  writeFileSync(stamp, `${JSON.stringify(nativeStamp(tools, user, database, true, migration))}\n`);
401
724
  return directory;
402
725
  }
726
+ /**
727
+ * How often the start looks for the postmaster's readiness (postmaster.pid's status line, then its port). Each look
728
+ * is two file reads; at 100 ms a start waited up to 100 ms past a ready server (Cal in a tab: ready at check 8, 1,211
729
+ * ms). The postmaster says it no other way a parent can wait on: its log is a file this start does not hold open.
730
+ */
731
+ const READY_CHECK_MS = 20;
403
732
  export async function nativePostgresUp(services, root, definition, launch) {
404
733
  if (services.length !== 1 || services[0].kind !== 'postgres')
405
734
  throw new Error('native postgres requires one declared postgres service');
406
735
  const deadline = Date.now() + READY_TIMEOUT_MS;
736
+ // EACH STEP OF THIS START SAYS ITS OWN TIME AND WHAT IT WAITED ON, as a note the World's up prints on its own
737
+ // clock. In a tab the whole start took ten seconds of a World's twelve (Twenty: backing chosen at +1.29 s, ready at
738
+ // +11.72 s) and one line, the cluster's placing, said where any of it went.
739
+ let stepBegan = performance.now();
740
+ const step = (what) => { const now = performance.now(); launch.note?.(`native PostgreSQL: ${what}, ${Math.round(now - stepBegan)} ms`); stepBegan = now; };
407
741
  const env = { ...(launch.env ?? process.env), LC_ALL: launch.env?.LC_ALL || 'C' };
408
742
  const binaries = nativePostgresTools(env);
409
743
  if (!binaries)
@@ -428,12 +762,19 @@ export async function nativePostgresUp(services, root, definition, launch) {
428
762
  const service = services[0];
429
763
  if (await listening(service.hostPort, deadline, launch.signal))
430
764
  throw new Error(`native postgres port ${service.hostPort} is already occupied`);
765
+ step(`an earlier start's programs retired and port ${service.hostPort} found free (one connection refused)`);
431
766
  const data = resolve(root, 'native-postgres-data');
432
767
  const { user, database } = settings(definition);
433
768
  // An empty data directory takes the World's prepared cluster when its stamp matches (ADR 0018's placement; the
434
769
  // cluster was made by these same programs at image build). initdb then finds PG_VERSION, and the database exists.
435
770
  forgetStaleMigration(data);
436
771
  const prepared = env.VOLTER_WORLD_CONFIG ? nativePreparedCluster(env.VOLTER_WORLD_CONFIG) : undefined;
772
+ // Placed this start under a stamp that names its database: the cluster holds it (S2 R15).
773
+ let placedDatabase = false;
774
+ // Copied as the server's own, every file already its: the walk below would find nothing to change.
775
+ let ownedAsCopied = false;
776
+ // Moved in whole this start, as the image laid it (S2 B2): owned by the owner the image names, not to be walked.
777
+ let movedAsLaid = false;
437
778
  // The check reads two files' metadata and the stamp; it runs only when a prepared cluster is there to take.
438
779
  if (prepared && existsSync(join(prepared.directory, 'PG_VERSION')) && !existsSync(join(data, 'PG_VERSION'))) {
439
780
  // A migrated cluster is the app's only for the step inputs it was migrated from (pglite-backing.ts, expectedMigration).
@@ -442,14 +783,34 @@ export async function nativePostgresUp(services, root, definition, launch) {
442
783
  launch.note?.(wanted.note);
443
784
  else {
444
785
  const expected = nativeStamp(binaries, user, database, false, wanted.migration);
445
- const placed = takePreparedCluster(prepared, text => nativeStampMatches(text, expected), data, 'other PostgreSQL programs, initdb arguments, identity or migration', wanted.migration);
786
+ const placed = takePreparedCluster(prepared, text => nativeStampMatches(text, expected), data, 'other PostgreSQL programs, initdb arguments, identity or migration', wanted.migration, copyClusterOwned(postgresRunAs()));
787
+ ownedAsCopied = placed.copied === true;
788
+ movedAsLaid = placed.placed && placed.copied !== true;
446
789
  if (placed.note)
447
790
  launch.note?.(placed.note);
791
+ placedDatabase = placed.placed;
448
792
  }
449
793
  }
794
+ // The placement's own parts, each a step (S2 R71: Cal's 195-206 ms was one line): the stamp's check and the move,
795
+ // then the server's directories and ownership with what the walk looked at and changed.
796
+ step(placedDatabase ? 'the prepared cluster\'s stamp read and matched, and the cluster moved into place' : 'the prepared cluster looked for (none placed)');
450
797
  mkdirSync(data, { recursive: true, mode: 0o700 });
798
+ const runAs = postgresRunAs();
799
+ postgresDirectorySearch(root, env, runAs);
800
+ // A cluster this start moved in whole from the prepared tree is owned as the image laid it. Where the page says the
801
+ // image laid every entry of that cluster with one owner, and that owner is the server's (VOLTER_WORLD_PREPARED_CLUSTER_
802
+ // OWNER, given only where its open placed the World's tree with the prepared project), the walk could change nothing
803
+ // and is not run (S2 B2: 134-206 ms of lstats on Cal and Twenty). Any other cluster, or no such word, is walked.
804
+ const laid = /^(\d+):(\d+)$/u.exec(env.VOLTER_WORLD_PREPARED_CLUSTER_OWNER ?? '');
805
+ const ownedAsLaid = movedAsLaid && laid !== null && Number(laid[1]) === runAs.uid && Number(laid[2]) === runAs.gid;
806
+ const owned = ownedAsCopied || ownedAsLaid ? undefined : ownPostgresCluster(data, runAs);
807
+ launch.note?.(`native PostgreSQL: child uid ${runAs.uid}, gid ${runAs.gid}; cluster ${data}`);
808
+ step(ownedAsCopied ? 'the prepared cluster looked for and copied as the server\'s own'
809
+ : ownedAsLaid ? `the prepared cluster looked for and placed where one matches, laid as the server's own (${runAs.uid}:${runAs.gid}): not walked`
810
+ : `the prepared cluster looked for and placed where one matches, its files made the server's (${owned.walked} entries walked, ${owned.changed} changed)`);
451
811
  if (!existsSync(join(data, 'PG_VERSION'))) {
452
- await tool(join(binaries, 'initdb'), ['-D', data, ...initdbArguments(user)], env, deadline, root, launch.signal);
812
+ await tool(join(binaries, 'initdb'), ['-D', data, ...initdbArguments(user)], env, deadline, root, runAs, launch.signal);
813
+ step('initdb run to its end (no prepared cluster was placed)');
453
814
  }
454
815
  launch.signal?.throwIfAborted();
455
816
  const binary = join(binaries, 'postgres');
@@ -463,7 +824,7 @@ export async function nativePostgresUp(services, root, definition, launch) {
463
824
  const fd = openSync(log, 'a');
464
825
  let child;
465
826
  try {
466
- child = spawn(binary, ['-D', data, '-h', '127.0.0.1', '-p', String(service.hostPort), '-k', ''], { env, detached: true, windowsHide: true, stdio: ['ignore', fd, fd] });
827
+ child = spawn(binary, ['-D', data, '-h', '127.0.0.1', '-p', String(service.hostPort), '-k', '', ...recoverySync()], { env, ...runAs, detached: true, windowsHide: true, stdio: ['ignore', fd, fd] });
467
828
  current.pid = child.pid ?? 0;
468
829
  started = { child, owned: current, closed: false };
469
830
  const observed = started;
@@ -487,7 +848,13 @@ export async function nativePostgresUp(services, root, definition, launch) {
487
848
  if (!child.pid)
488
849
  throw new Error('native postgres spawn failed');
489
850
  publishProcessReceipt(recordPath(root), JSON.stringify(current) + '\n');
851
+ step('the postmaster spawned, its identity captured and its receipts written');
852
+ // The cluster is chosen and placed (takePreparedCluster's move or copy, its migration record written; or initdb's)
853
+ // and its server spawned: what a twin's staged store is placed beside is there now (S2 R79), before readiness.
854
+ launch.onPlaced?.();
855
+ let checks = 0;
490
856
  for (;;) {
857
+ checks += 1;
491
858
  launch.signal?.throwIfAborted();
492
859
  if (started.error)
493
860
  throw started.error;
@@ -495,19 +862,33 @@ export async function nativePostgresUp(services, root, definition, launch) {
495
862
  throw new Error(`native postgres exited before readiness; see ${log}${logTail(log)}`);
496
863
  if (Date.now() >= deadline)
497
864
  throw new Error(`native postgres did not become ready; see ${log}${logTail(log)}`);
498
- if (existsSync(join(data, 'postmaster.pid'))) {
499
- verifyOwner(current);
500
- const lines = readFileSync(join(data, 'postmaster.pid'), 'utf8').split('\n');
865
+ if (existsSync(join(data, 'postmaster.pid')) && verifyOwner(current)) {
866
+ // Read again for its status line; gone or shorter between the two reads is not ready yet.
867
+ let lines = [];
868
+ try {
869
+ lines = readFileSync(join(data, 'postmaster.pid'), 'utf8').split('\n');
870
+ }
871
+ catch { /* removed since the check above */ }
501
872
  if (lines[7]?.trim() === 'ready' && await listening(service.hostPort, deadline, launch.signal))
502
873
  break;
503
874
  }
504
- await wait(100);
875
+ await wait(READY_CHECK_MS);
505
876
  }
506
- if (database !== 'postgres') {
877
+ step(`the postmaster ready: postmaster.pid says so and its port answers, at check ${checks} (a check every ${READY_CHECK_MS} ms)`);
878
+ // A cluster placed this start was matched on a stamp that names this database (nativeStampMatches), and made with
879
+ // it by these same programs at image build: asking the server again was a psql process of its own (Cal in a tab:
880
+ // 327 ms of a 2.3 s World up). A cluster from an earlier start, or initdb's, is still asked, and given the database
881
+ // where it lacks it.
882
+ if (placedDatabase && database !== 'postgres')
883
+ step(`database ${database} is the placed cluster's, by its stamp: not asked`);
884
+ else if (database !== 'postgres') {
507
885
  const sql = `SELECT 1 FROM pg_database WHERE datname = '${database.replaceAll("'", "''")}'`;
508
- const output = await tool(join(binaries, 'psql'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '-d', 'postgres', '-tAc', sql], env, deadline, root, launch.signal);
509
- if (!output.trim())
510
- await tool(join(binaries, 'createdb'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '--', database], env, deadline, root, launch.signal);
886
+ const output = await tool(join(binaries, 'psql'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '-d', 'postgres', '-tAc', sql], env, deadline, root, runAs, launch.signal);
887
+ step(`psql run to its end, asking whether database ${database} is there`);
888
+ if (!output.trim()) {
889
+ await tool(join(binaries, 'createdb'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '--', database], env, deadline, root, runAs, launch.signal);
890
+ step(`createdb run to its end`);
891
+ }
511
892
  }
512
893
  launch.signal?.throwIfAborted();
513
894
  if (child.exitCode !== null || child.signalCode !== null)
@@ -520,7 +901,7 @@ export async function nativePostgresUp(services, root, definition, launch) {
520
901
  const failures = [error];
521
902
  try {
522
903
  if (started)
523
- await retireStarted(started);
904
+ await retireStarted(started, 'up (startup failed)');
524
905
  removeReceipts(root);
525
906
  starts.delete(root);
526
907
  }
@@ -532,7 +913,9 @@ export async function nativePostgresUp(services, root, definition, launch) {
532
913
  throw error;
533
914
  }
534
915
  }
535
- export async function nativePostgresStatus(root) {
916
+ /** The native postmaster's status proof: its ownership known, its held group live, its spawn identity and owner
917
+ * verified, and its port answering. `signal` ends the port's wait with the boot that asks (an up's hand-over). */
918
+ export async function nativePostgresStatus(root, signal) {
536
919
  const state = ownership(root);
537
920
  if (state.errors.length)
538
921
  throw new AggregateError(state.errors, 'native postgres ownership uncertain; retry down');
@@ -540,8 +923,9 @@ export async function nativePostgresStatus(root) {
540
923
  return false;
541
924
  if (state.identity)
542
925
  await verifySpawnIdentity(state.identity);
543
- verifyOwner(state.held);
544
- return listening(state.held.port, Date.now() + READY_TIMEOUT_MS);
926
+ if (!verifyOwner(state.held))
927
+ return false;
928
+ return listening(state.held.port, Date.now() + READY_TIMEOUT_MS, signal);
545
929
  }
546
930
  async function retireTools(root) {
547
931
  if (!existsSync(root))
@@ -570,18 +954,19 @@ async function retireTools(root) {
570
954
  rmSync(spawnReceiptPath(receipt), { force: true });
571
955
  }
572
956
  }
573
- export async function nativePostgresDown(root) {
957
+ export async function nativePostgresDown(root, by = 'down') {
574
958
  const started = starts.get(root);
575
959
  const state = ownership(root);
960
+ const data = resolve(root, 'native-postgres-data');
576
961
  if (state.errors.length)
577
962
  process.stderr.write(`${errorMessage(new AggregateError(state.errors, 'native postgres ownership uncertain; retrying retirement through established identity'))}\n`);
578
963
  if (started) {
579
- await retireStarted(started);
964
+ await retireStarted(started, by);
580
965
  }
581
966
  else if (state.identity) {
582
967
  if (liveGroup(state.identity.pid)) {
583
968
  await verifySpawnIdentity(state.identity);
584
- await retireIdentity(state.identity);
969
+ await retirePostmaster(state.identity, by, data);
585
970
  }
586
971
  }
587
972
  else if (state.held && state.held.pid > 0) {