@substrat-run/cli 0.43.0 → 0.45.0

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.
@@ -0,0 +1,70 @@
1
+ import type { ChildMessage, ReplayRequest } from './data-loss-replay-child.js';
2
+ /**
3
+ * How long one migration may run. A Durable Object would stop a migration that ran this long
4
+ * on its own CPU limit, so a replay that has not finished by then is not one worth waiting for.
5
+ */
6
+ export declare const MIGRATION_TIMEOUT_MS = 30000;
7
+ /** What the push hands over: read off the same import that yields the permission surface. */
8
+ export interface ReplayInputs {
9
+ /** The vertical's own `@substrat-run/kernel`, as imported from its directory, or null. */
10
+ readonly kernel: unknown;
11
+ /** Where that kernel resolves, for the replay's own process to import — or null. */
12
+ readonly kernelUrl: string | null;
13
+ /** The permission entry's `modules`, each a registration carrying its `migrations`. */
14
+ readonly modules: readonly unknown[];
15
+ /** The vertical's adapter-sqlite `SCOPE_SPINE_DDL`, resolved only when a replay runs. */
16
+ readonly spineDdl: () => Promise<string | undefined>;
17
+ }
18
+ export type DataLossCheck = {
19
+ readonly status: 'passed';
20
+ readonly authored: number;
21
+ } | {
22
+ readonly status: 'skipped';
23
+ readonly why: string;
24
+ } | {
25
+ readonly status: 'incomplete';
26
+ readonly authored: number;
27
+ readonly notReplayed: number;
28
+ readonly key: string;
29
+ readonly message: string;
30
+ };
31
+ type ChildResult = Extract<ChildMessage, {
32
+ type: 'done';
33
+ }>['result'] | {
34
+ readonly kind: 'timeout';
35
+ readonly key: string | undefined;
36
+ readonly passed: number;
37
+ };
38
+ /**
39
+ * Run the replay in its own process, killing it when one migration runs past `timeoutMs`. Each
40
+ * `started` line restarts the clock, so the limit is per migration, and says which one was
41
+ * running when it fired and how many authored migrations had passed before it.
42
+ */
43
+ export declare function replayInChild(request: ReplayRequest, opts?: {
44
+ readonly childPath?: string;
45
+ readonly timeoutMs?: number;
46
+ }): Promise<ChildResult>;
47
+ /**
48
+ * Is the check required to have run: the flag, or `SUBSTRAT_REQUIRE_DATA_LOSS_CHECK` set to `1`,
49
+ * `true` or `yes` (any case). Any other non-empty value is warned about rather than read as no,
50
+ * because a CI job that meant yes would otherwise get the permissive default without a word.
51
+ */
52
+ export declare function dataLossCheckRequired(argv: readonly string[], env?: NodeJS.ProcessEnv, warn?: (message: string) => void): boolean;
53
+ /** The one summary line, the same in the check's own output and the push's last line. */
54
+ export declare function formatDataLossCheck(check: DataLossCheck): string;
55
+ /**
56
+ * Replay the vertical's migrations through its own kernel's fence. Throws when the fence would
57
+ * refuse one, and — with `require` — when the check was skipped or incomplete. Otherwise returns
58
+ * the outcome, having printed each declared move and discard with its reason (`log`) and the
59
+ * summary (`log` when passed, `warn` — stderr — when it is not).
60
+ */
61
+ export declare function checkMigrationDataLoss(inputs: ReplayInputs, opts?: {
62
+ readonly require?: boolean;
63
+ readonly log?: (message: string) => void;
64
+ readonly warn?: (message: string) => void;
65
+ /** The replay process's entry and per-migration limit; the defaults are the shipped ones. */
66
+ readonly childPath?: string;
67
+ readonly timeoutMs?: number;
68
+ }): Promise<DataLossCheck>;
69
+ export {};
70
+ //# sourceMappingURL=data-loss-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-loss-check.d.ts","sourceRoot":"","sources":["../src/data-loss-check.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAK/E;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACtD;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACpD;IACE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AA2BN,KAAK,WAAW,GACZ,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAK5F;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACtE,OAAO,CAAC,WAAW,CAAC,CA0EtB;AAOD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAoB,GAC9C,OAAO,CAUT;AAED,yFAAyF;AACzF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAYhE;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE;IACJ,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CACxB,GACL,OAAO,CAAC,aAAa,CAAC,CAuBxB"}
@@ -0,0 +1,260 @@
1
+ /**
2
+ * The data-loss check a push makes before upload (#2321): the vertical's authored migrations,
3
+ * replayed against an in-memory scope that holds data, through the data-loss fence (#115).
4
+ *
5
+ * The fence refuses, at apply, a migration that would lose data a scope holds and does not
6
+ * declare it — which closes every populated scope until a fixed version ships. In this repository
7
+ * `lint:migration-fence` asks the question in the pull request; a vertical built anywhere else
8
+ * has only this, the push, as the earlier and cheaper place to hear no.
9
+ *
10
+ * The replay is the VERTICAL's own kernel's (`replayMigrations`), reached at run time the way
11
+ * `moduleMigrations` is (#1677): it is the copy of the fence the vertical's scopes run at apply,
12
+ * so it is the one whose answer matters, and this CLI stays Apache-2.0 with no kernel dependency.
13
+ * What this module supplies is only what the kernel does not import: an empty in-memory SQLite
14
+ * (`node:sqlite`), and the platform's spine tables from the vertical's own
15
+ * `@substrat-run/adapter-sqlite`, when it has one, for an authored migration that reads them.
16
+ *
17
+ * It runs in a process of its own (`data-loss-replay-child.ts`), with an empty environment and a
18
+ * time limit per migration, because a runaway statement — a recursive CTE with no end — runs in
19
+ * native code nothing in this process could interrupt. The fence refuses ATTACH, so the replay
20
+ * never reaches a file; it opens no network connection and reads none of the vertical's data.
21
+ *
22
+ * Three outcomes, never confused:
23
+ * - REFUSED — the fence would refuse a migration. The push stops, with the fence's own message
24
+ * naming the declaration (`moves` / `discards`) to add. There is no flag past it: a `discards`
25
+ * entry with its reason is the reviewed way to lose data on purpose.
26
+ * - INCOMPLETE — a migration could not be run on synthetic rows at all (its SQL needs real input,
27
+ * reads a spine table this CLI could not build, or did not finish in time). The replay stops
28
+ * there, so the migrations after it go unchecked, and the warning says how many.
29
+ * - SKIPPED — nothing could be replayed: no `node:sqlite` (Node before 22.13), or a kernel with
30
+ * no replay this CLI knows how to call.
31
+ * The last two do not refuse by default — promote is the hard gate, and the fence still refuses
32
+ * there — unless the caller asks for the guarantee (`--require-data-loss-check`).
33
+ */
34
+ import { spawn } from 'node:child_process';
35
+ import { tmpdir } from 'node:os';
36
+ import { fileURLToPath } from 'node:url';
37
+ /** The replay API this CLI calls — `MIGRATION_REPLAY_API` in `@substrat-run/kernel`. */
38
+ const REPLAY_API = 1;
39
+ /**
40
+ * How long one migration may run. A Durable Object would stop a migration that ran this long
41
+ * on its own CPU limit, so a replay that has not finished by then is not one worth waiting for.
42
+ */
43
+ export const MIGRATION_TIMEOUT_MS = 30_000;
44
+ const isOutcome = (o) => {
45
+ const r = o;
46
+ const stop = (s) => s === undefined || (typeof s.key === 'string' && typeof s.message === 'string');
47
+ return (typeof r === 'object' && r !== null &&
48
+ typeof r.authored === 'number' && typeof r.passed === 'number' &&
49
+ Array.isArray(r.declared) && r.declared.every((d) => typeof d === 'string') &&
50
+ stop(r.refused) && stop(r.failed));
51
+ };
52
+ /** The built child, beside this module in `dist`. */
53
+ const CHILD = fileURLToPath(new URL('./data-loss-replay-child.js', import.meta.url));
54
+ /**
55
+ * Run the replay in its own process, killing it when one migration runs past `timeoutMs`. Each
56
+ * `started` line restarts the clock, so the limit is per migration, and says which one was
57
+ * running when it fired and how many authored migrations had passed before it.
58
+ */
59
+ export function replayInChild(request, opts = {}) {
60
+ const timeoutMs = opts.timeoutMs ?? MIGRATION_TIMEOUT_MS;
61
+ // The child's stderr is kept, never forwarded: node's notice that `node:sqlite` is experimental
62
+ // stays out of the push log, and only a process that dies without answering is shown it.
63
+ const child = spawn(process.execPath, [opts.childPath ?? CHILD], {
64
+ cwd: tmpdir(),
65
+ // Nothing of the pusher's environment: the replay needs none of it.
66
+ env: process.platform === 'win32' && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {},
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ });
69
+ return new Promise((resolveResult) => {
70
+ let settled = false;
71
+ let key;
72
+ let authoredStarted = 0;
73
+ let runningAuthored = false;
74
+ let stdout = '';
75
+ let stderr = '';
76
+ let timer;
77
+ // A push that ends any other way — it exits, throws, or is killed by a signal, SIGKILL
78
+ // included — takes the replay with it through the child's own watchdog
79
+ // (`data-loss-replay-child.ts`), which no handler here could do for SIGKILL. On POSIX; on
80
+ // Windows, where an orphan is not re-parented, only the child's hard deadline stops it.
81
+ const settle = (result) => {
82
+ if (settled)
83
+ return;
84
+ settled = true;
85
+ clearTimeout(timer);
86
+ if (child.exitCode !== null || child.signalCode !== null) {
87
+ resolveResult(result);
88
+ return;
89
+ }
90
+ // Answered only once the process is gone, so nothing the replay started outlives it.
91
+ child.once('exit', () => resolveResult(result));
92
+ child.kill('SIGKILL');
93
+ };
94
+ const arm = () => {
95
+ clearTimeout(timer);
96
+ timer = setTimeout(() => settle({ kind: 'timeout', key, passed: authoredStarted - (runningAuthored ? 1 : 0) }), timeoutMs);
97
+ };
98
+ child.stdout.setEncoding('utf8');
99
+ child.stdout.on('data', (chunk) => {
100
+ stdout += chunk;
101
+ let nl;
102
+ while ((nl = stdout.indexOf('\n')) >= 0) {
103
+ const line = stdout.slice(0, nl);
104
+ stdout = stdout.slice(nl + 1);
105
+ let message;
106
+ try {
107
+ message = JSON.parse(line);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ if (message.type === 'started') {
113
+ key = message.key;
114
+ runningAuthored = message.authored;
115
+ if (message.authored)
116
+ authoredStarted += 1;
117
+ arm();
118
+ }
119
+ else if (message.type === 'done') {
120
+ settle(message.result);
121
+ }
122
+ }
123
+ });
124
+ child.stderr.setEncoding('utf8');
125
+ child.stderr.on('data', (chunk) => {
126
+ stderr = (stderr + chunk).slice(-2000);
127
+ });
128
+ child.on('error', (e) => settle({ kind: 'error', message: e.message }));
129
+ // `close`, not `exit`: it fires once stdout has drained, so a last `done` line is never lost.
130
+ child.on('close', (code, signal) => settle({ kind: 'error', message: `the replay process ended (${signal ?? `exit ${code}`}) without an answer${stderr ? `: ${stderr.trim()}` : ''}` }));
131
+ arm();
132
+ child.stdin.on('error', () => { });
133
+ child.stdin.end(JSON.stringify(request));
134
+ });
135
+ }
136
+ const DOCS = 'see https://substrat.net/concepts/deploying ("A migration cannot quietly lose data")';
137
+ const TRUTHY = /^(1|true|yes)$/i;
138
+ const FALSY = /^(0|false|no)?$/i;
139
+ /**
140
+ * Is the check required to have run: the flag, or `SUBSTRAT_REQUIRE_DATA_LOSS_CHECK` set to `1`,
141
+ * `true` or `yes` (any case). Any other non-empty value is warned about rather than read as no,
142
+ * because a CI job that meant yes would otherwise get the permissive default without a word.
143
+ */
144
+ export function dataLossCheckRequired(argv, env = process.env, warn = console.error) {
145
+ if (argv.includes('--require-data-loss-check'))
146
+ return true;
147
+ const value = (env.SUBSTRAT_REQUIRE_DATA_LOSS_CHECK ?? '').trim();
148
+ if (TRUTHY.test(value))
149
+ return true;
150
+ if (!FALSY.test(value)) {
151
+ warn(`⚠ SUBSTRAT_REQUIRE_DATA_LOSS_CHECK=${value} is not one of 1/true/yes or 0/false/no — read as no, so the data-loss check stays optional`);
152
+ }
153
+ return false;
154
+ }
155
+ /** The one summary line, the same in the check's own output and the push's last line. */
156
+ export function formatDataLossCheck(check) {
157
+ switch (check.status) {
158
+ case 'passed':
159
+ return `data-loss check: PASSED (${check.authored} authored migration(s) replayed against a populated scope)`;
160
+ case 'skipped':
161
+ return `data-loss check: SKIPPED (${check.why}) — the fence still refuses at promote`;
162
+ case 'incomplete':
163
+ return (`data-loss check: INCOMPLETE (${check.notReplayed} of ${check.authored} authored migration(s) were not replayed ` +
164
+ `after ${check.key} failed) — the fence still refuses at promote`);
165
+ }
166
+ }
167
+ /**
168
+ * Replay the vertical's migrations through its own kernel's fence. Throws when the fence would
169
+ * refuse one, and — with `require` — when the check was skipped or incomplete. Otherwise returns
170
+ * the outcome, having printed each declared move and discard with its reason (`log`) and the
171
+ * summary (`log` when passed, `warn` — stderr — when it is not).
172
+ */
173
+ export async function checkMigrationDataLoss(inputs, opts = {}) {
174
+ const log = opts.log ?? console.log;
175
+ const warn = opts.warn ?? console.error;
176
+ const check = await replay(inputs, opts, log);
177
+ const line = formatDataLossCheck(check);
178
+ if (check.status === 'passed') {
179
+ log(line);
180
+ return check;
181
+ }
182
+ if (check.status === 'incomplete') {
183
+ warn(`⚠ ${check.key} could not be replayed on synthetic data: ${check.message}`);
184
+ if (/no such table: _substrat_/i.test(check.message)) {
185
+ warn(' it reads a platform table this CLI could not build: add @substrat-run/adapter-sqlite as a devDependency ' +
186
+ 'so the replay can create them.');
187
+ }
188
+ }
189
+ if (opts.require) {
190
+ throw new Error(`${line}.\n--require-data-loss-check (or SUBSTRAT_REQUIRE_DATA_LOSS_CHECK=1) refuses a push it could not fully check.`);
191
+ }
192
+ warn(`⚠ ${line}`);
193
+ return check;
194
+ }
195
+ async function replay(inputs, opts, log) {
196
+ const kernel = inputs.kernel;
197
+ const authoredCount = inputs.modules.reduce((n, m) => {
198
+ const migrations = m.migrations;
199
+ return n + (Array.isArray(migrations) ? migrations.length : 0);
200
+ }, 0);
201
+ // Nothing authored, nothing the fence judges — and no reason to need SQLite.
202
+ if (authoredCount === 0)
203
+ return { status: 'passed', authored: 0 };
204
+ if (!kernel || !inputs.kernelUrl)
205
+ return { status: 'skipped', why: 'no @substrat-run/kernel resolves from the vertical' };
206
+ if (typeof kernel.replayMigrations !== 'function') {
207
+ return { status: 'skipped', why: "the vertical's @substrat-run/kernel has no migration replay (upgrade it)" };
208
+ }
209
+ if (kernel.MIGRATION_REPLAY_API !== REPLAY_API) {
210
+ return {
211
+ status: 'skipped',
212
+ why: `the vertical's @substrat-run/kernel has migration replay API ${String(kernel.MIGRATION_REPLAY_API)}, this CLI calls ${REPLAY_API} (upgrade the CLI)`,
213
+ };
214
+ }
215
+ // The registrations as data — what the replay reads of them, without the handlers no
216
+ // process boundary can carry.
217
+ const modules = JSON.parse(JSON.stringify(inputs.modules.map((m) => ({ manifest: m.manifest, migrations: m.migrations }))));
218
+ const timeoutMs = opts.timeoutMs ?? MIGRATION_TIMEOUT_MS;
219
+ // The child's own backstop, far past anything the per-migration limit allows: every authored
220
+ // migration and up to three derived ones per module (search, entity state, list), each at the
221
+ // limit, plus one for the start.
222
+ const deadlineMs = timeoutMs * (authoredCount + 3 * modules.length + 1);
223
+ const result = await replayInChild({ kernelUrl: inputs.kernelUrl, modules, spineDdl: await inputs.spineDdl(), deadlineMs }, { ...opts, timeoutMs });
224
+ if (result.kind === 'no-sqlite') {
225
+ return { status: 'skipped', why: `node:sqlite unavailable on Node ${process.versions.node}; it needs 22.13 or later` };
226
+ }
227
+ if (result.kind === 'error')
228
+ return { status: 'skipped', why: `the replay itself failed: ${result.message}` };
229
+ if (result.kind === 'timeout') {
230
+ return {
231
+ status: 'incomplete',
232
+ authored: authoredCount,
233
+ notReplayed: authoredCount - result.passed,
234
+ key: result.key ?? 'the replay',
235
+ message: `it did not finish within ${timeoutMs / 1000} s and was stopped — a statement with no end, such as a recursive CTE ` +
236
+ "without a limit? A Durable Object's own CPU limit would stop it at promote too",
237
+ };
238
+ }
239
+ const outcome = result.outcome;
240
+ if (!isOutcome(outcome))
241
+ return { status: 'skipped', why: "the vertical's kernel returned a replay outcome this CLI does not recognise" };
242
+ for (const line of outcome.declared)
243
+ log(`declared ${line}`);
244
+ if (outcome.refused) {
245
+ throw new Error(`data-loss check: REFUSED — ${outcome.refused.message}\n` +
246
+ 'A scope holding that data would refuse this migration at promote and fail closed. Fix the migration, or ' +
247
+ `declare the loss on it (\`moves\` / \`discards\`, with its reason) for review — ${DOCS}.`);
248
+ }
249
+ if (outcome.failed) {
250
+ return {
251
+ status: 'incomplete',
252
+ authored: outcome.authored,
253
+ notReplayed: outcome.authored - outcome.passed,
254
+ key: outcome.failed.key,
255
+ message: outcome.failed.message,
256
+ };
257
+ }
258
+ return { status: 'passed', authored: outcome.authored };
259
+ }
260
+ //# sourceMappingURL=data-loss-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-loss-check.js","sourceRoot":"","sources":["../src/data-loss-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,wFAAwF;AACxF,MAAM,UAAU,GAAG,CAAC,CAAC;AAErB;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAsC3C,MAAM,SAAS,GAAG,CAAC,CAAU,EAAsB,EAAE;IACnD,MAAM,CAAC,GAAG,CAAyB,CAAC;IACpC,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,EAAE,CAC1B,CAAC,KAAK,SAAS,IAAI,CAAC,OAAQ,CAAuB,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAQ,CAA2B,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC;IACpI,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;QACnC,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAC9D,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QAC3E,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAClC,CAAC;AACJ,CAAC,CAAC;AAMF,qDAAqD;AACrD,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,6BAA6B,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAErF;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAsB,EACtB,IAAI,GAAiE,EAAE;IAEvE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,oBAAoB,CAAC;IACzD,gGAAgG;IAChG,yFAAyF;IACzF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,EAAE;QAC/D,GAAG,EAAE,MAAM,EAAE;QACb,oEAAoE;QACpE,GAAG,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE;QACzG,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;KAChC,CAAC,CAAC;IACH,OAAO,IAAI,OAAO,CAAC,CAAC,aAAa,EAAE,EAAE;QACnC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,GAAuB,CAAC;QAC5B,IAAI,eAAe,GAAG,CAAC,CAAC;QACxB,IAAI,eAAe,GAAG,KAAK,CAAC;QAC5B,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,KAAiC,CAAC;QACtC,uFAAuF;QACvF,uEAAuE;QACvE,0FAA0F;QAC1F,wFAAwF;QACxF,MAAM,MAAM,GAAG,CAAC,MAAmB,EAAQ,EAAE;YAC3C,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;gBACzD,aAAa,CAAC,MAAM,CAAC,CAAC;gBACtB,OAAO;YACT,CAAC;YACD,qFAAqF;YACrF,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;YAChD,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC,CAAC;QACF,MAAM,GAAG,GAAG,GAAS,EAAE;YACrB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,eAAe,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;QAC7H,CAAC,CAAC;QACF,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,IAAI,KAAK,CAAC;YAChB,IAAI,EAAU,CAAC;YACf,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACjC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;gBAC9B,IAAI,OAAqB,CAAC;gBAC1B,IAAI,CAAC;oBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAiB,CAAC;gBAC7C,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBAC/B,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;oBAClB,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC;oBACnC,IAAI,OAAO,CAAC,QAAQ;wBAAE,eAAe,IAAI,CAAC,CAAC;oBAC3C,GAAG,EAAE,CAAC;gBACR,CAAC;qBAAM,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBACnC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACxE,8FAA8F;QAC9F,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CACjC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,6BAA6B,MAAM,IAAI,QAAQ,IAAI,EAAE,sBAAsB,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CACpJ,CAAC;QACF,GAAG,EAAE,CAAC;QACN,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,IAAI,GAAG,sFAAsF,CAAC;AAEpG,MAAM,MAAM,GAAG,iBAAiB,CAAC;AACjC,MAAM,KAAK,GAAG,kBAAkB,CAAC;AAEjC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAuB,EACvB,GAAG,GAAsB,OAAO,CAAC,GAAG,EACpC,IAAI,GAA8B,OAAO,CAAC,KAAK;IAE/C,IAAI,IAAI,CAAC,QAAQ,CAAC,2BAA2B,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,IAAI,CACF,sCAAsC,KAAK,6FAA6F,CACzI,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,mBAAmB,CAAC,KAAoB;IACtD,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;QACrB,KAAK,QAAQ;YACX,OAAO,4BAA4B,KAAK,CAAC,QAAQ,4DAA4D,CAAC;QAChH,KAAK,SAAS;YACZ,OAAO,6BAA6B,KAAK,CAAC,GAAG,wCAAwC,CAAC;QACxF,KAAK,YAAY;YACf,OAAO,CACL,gCAAgC,KAAK,CAAC,WAAW,OAAO,KAAK,CAAC,QAAQ,2CAA2C;gBACjH,SAAS,KAAK,CAAC,GAAG,+CAA+C,CAClE,CAAC;IACN,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAoB,EACpB,IAAI,GAOA,EAAE;IAEN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,CAAC;QACV,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QAClC,IAAI,CAAC,KAAK,KAAK,CAAC,GAAG,6CAA6C,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACjF,IAAI,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACrD,IAAI,CACF,4GAA4G;gBAC1G,gCAAgC,CACnC,CAAC;QACJ,CAAC;IACH,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,+GAA+G,CAAC,CAAC;IAC1I,CAAC;IACD,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAClB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,MAAM,CACnB,MAAoB,EACpB,IAAkE,EAClE,GAA8B;IAE9B,MAAM,MAAM,GAAG,MAAM,CAAC,MAA6B,CAAC;IACpD,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAS,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3D,MAAM,UAAU,GAAI,CAA8B,CAAC,UAAU,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC,EAAE,CAAC,CAAC,CAAC;IACN,6EAA6E;IAC7E,IAAI,aAAa,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IAClE,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,oDAAoD,EAAE,CAAC;IAC1H,IAAI,OAAO,MAAM,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;QAClD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,0EAA0E,EAAE,CAAC;IAChH,CAAC;IACD,IAAI,MAAM,CAAC,oBAAoB,KAAK,UAAU,EAAE,CAAC;QAC/C,OAAO;YACL,MAAM,EAAE,SAAS;YACjB,GAAG,EAAE,gEAAgE,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,oBAAoB,UAAU,oBAAoB;SAC3J,CAAC;IACJ,CAAC;IACD,qFAAqF;IACrF,8BAA8B;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAG,CAA4B,CAAC,QAAQ,EAAE,UAAU,EAAG,CAA8B,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAC7I,CAAC;IACf,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,oBAAoB,CAAC;IACzD,6FAA6F;IAC7F,8FAA8F;IAC9F,iCAAiC;IACjC,MAAM,UAAU,GAAG,SAAS,GAAG,CAAC,aAAa,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,MAAM,aAAa,CAChC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,EACvF,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CACvB,CAAC;IACF,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,mCAAmC,OAAO,CAAC,QAAQ,CAAC,IAAI,2BAA2B,EAAE,CAAC;IACzH,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,6BAA6B,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;IAC9G,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE,YAAY;YACpB,QAAQ,EAAE,aAAa;YACvB,WAAW,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM;YAC1C,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,YAAY;YAC/B,OAAO,EACL,4BAA4B,SAAS,GAAG,IAAI,wEAAwE;gBACpH,gFAAgF;SACnF,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,6EAA6E,EAAE,CAAC;IAC1I,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,QAAQ;QAAE,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,8BAA8B,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI;YACvD,0GAA0G;YAC1G,mFAAmF,IAAI,GAAG,CAC7F,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,OAAO;YACL,MAAM,EAAE,YAAY;YACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,WAAW,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,MAAM;YAC9C,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG;YACvB,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;SAChC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,27 @@
1
+ export interface ReplayRequest {
2
+ readonly kernelUrl: string;
3
+ readonly modules: readonly unknown[];
4
+ readonly spineDdl?: string;
5
+ /** Wall-clock limit for the whole replay, after which the child kills itself — a backstop. */
6
+ readonly deadlineMs: number;
7
+ }
8
+ /** How often the watchdog looks at its parent. */
9
+ export declare const WATCHDOG_POLL_MS = 200;
10
+ /** Each stdout line, in order: zero or more `started`, then one `done`. */
11
+ export type ChildMessage = {
12
+ readonly type: 'started';
13
+ readonly key: string;
14
+ readonly authored: boolean;
15
+ } | {
16
+ readonly type: 'done';
17
+ readonly result: {
18
+ readonly kind: 'outcome';
19
+ readonly outcome: unknown;
20
+ } | {
21
+ readonly kind: 'no-sqlite';
22
+ } | {
23
+ readonly kind: 'error';
24
+ readonly message: string;
25
+ };
26
+ };
27
+ //# sourceMappingURL=data-loss-replay-child.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-loss-replay-child.d.ts","sourceRoot":"","sources":["../src/data-loss-replay-child.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,8FAA8F;IAC9F,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAepC,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAC9E;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EACX;QAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GACvD;QAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;KAAE,GAC9B;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The data-loss replay's own process (#2321): what `checkMigrationDataLoss` runs the vertical's
3
+ * migrations in, so a runaway one can be stopped. A SQLite statement runs in native code that no
4
+ * JavaScript timer and no `worker.terminate()` can interrupt, and this Node's `node:sqlite` has no
5
+ * progress handler; a process can always be killed. See `replayInChild` for the parent's half.
6
+ *
7
+ * Reads one request as JSON on stdin — the vertical's kernel URL, its modules as data, the spine
8
+ * DDL — and writes one JSON line per message to stdout, synchronously, so the line naming the
9
+ * migration about to run is out before that migration can block the process. Started with an empty
10
+ * environment, in the temp directory, against an in-memory database.
11
+ *
12
+ * It must not outlive the push that started it, however that push ends. The parent kills it on a
13
+ * timeout, but a parent killed by a signal runs no handler at all, and an orphaned replay blocked
14
+ * in a SQLite step would spin forever. So the child carries a watchdog of
15
+ * its own: a worker thread — which a blocked main thread cannot starve — that kills this process
16
+ * when its parent is gone (the parent pid changes as the OS re-parents it) or the request's hard
17
+ * deadline passes. Covered: the parent exiting, crashing, or being killed by any signal, SIGKILL
18
+ * included, within the watchdog's poll interval. Not covered: a parent that is alive but stopped
19
+ * (SIGSTOP) — the replay then runs until the deadline — and, because the parent check relies on
20
+ * POSIX re-parenting an orphan, Windows, where only the deadline stops an orphaned replay.
21
+ */
22
+ import { writeSync } from 'node:fs';
23
+ import { Worker } from 'node:worker_threads';
24
+ /** How often the watchdog looks at its parent. */
25
+ export const WATCHDOG_POLL_MS = 200;
26
+ /** Kill this process when the parent that started it is gone, or `deadlineMs` has passed. */
27
+ function watchdog(deadlineMs) {
28
+ const worker = new Worker(`const { workerData: { parent, until, every } } = require('node:worker_threads');
29
+ setInterval(() => {
30
+ if (process.ppid !== parent || Date.now() > until) process.kill(process.pid, 'SIGKILL');
31
+ }, every);`, { eval: true, workerData: { parent: process.ppid, until: Date.now() + deadlineMs, every: WATCHDOG_POLL_MS } });
32
+ // The watchdog never keeps a finished replay alive.
33
+ worker.unref();
34
+ }
35
+ const send = (message) => {
36
+ writeSync(1, `${JSON.stringify(message)}\n`);
37
+ };
38
+ async function main() {
39
+ const chunks = [];
40
+ for await (const chunk of process.stdin)
41
+ chunks.push(chunk);
42
+ const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
43
+ watchdog(request.deadlineMs);
44
+ let open;
45
+ try {
46
+ const { DatabaseSync } = (await import('node:sqlite'));
47
+ open = () => new DatabaseSync(':memory:');
48
+ }
49
+ catch {
50
+ send({ type: 'done', result: { kind: 'no-sqlite' } });
51
+ return;
52
+ }
53
+ try {
54
+ const kernel = (await import(request.kernelUrl));
55
+ const db = open();
56
+ const outcome = kernel.replayMigrations({
57
+ exec: (q) => db.exec(q),
58
+ query: (q, p = []) => db.prepare(q).all(...p),
59
+ run: (q, p = []) => ({ changes: Number(db.prepare(q).run(...p).changes) }),
60
+ }, request.modules, {
61
+ spineDdl: request.spineDdl,
62
+ onMigration: (key, authored) => send({ type: 'started', key, authored }),
63
+ });
64
+ send({ type: 'done', result: { kind: 'outcome', outcome } });
65
+ }
66
+ catch (e) {
67
+ send({ type: 'done', result: { kind: 'error', message: e instanceof Error ? e.message : String(e) } });
68
+ }
69
+ }
70
+ await main();
71
+ //# sourceMappingURL=data-loss-replay-child.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-loss-replay-child.js","sourceRoot":"","sources":["../src/data-loss-replay-child.ts"],"names":[],"mappings":"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"}
package/dist/promote.d.ts CHANGED
@@ -12,9 +12,14 @@
12
12
  * #1705 PR 3: a promote that drops or re-versions an exported event an installed app imports
13
13
  * is refused too (409), and the refusal lists the apps it would break: the caller's own tenant's
14
14
  * by name, any other tenant only as a count. `--ack-export-break` passes it once that is read.
15
+ *
16
+ * #2333: an acknowledgement answers ONE review — the diffs printed against the version prod served
17
+ * when they were read. Every refusal that prints them also prints the `--review` token they were
18
+ * read with, and the control plane refuses a token whose base or stored rows have moved since
19
+ * (and a `--ack-migrations` sent with none), so a person never acknowledges a diff they were not shown.
15
20
  */
16
21
  import { type MigrationDiff, type PermissionRegistry } from '@substrat-run/contracts';
17
- import { type ExportBreaks, type MintedStore, type PromoteResult } from '@substrat-run/control-plane-client';
22
+ import { type ExportBreaks, type MintedStore, type PromoteResult, type PromoteReviewBinding } from '@substrat-run/control-plane-client';
18
23
  export interface PromoteOptions {
19
24
  controlPlaneUrl: string;
20
25
  header: Record<string, string>;
@@ -25,8 +30,13 @@ export interface PromoteOptions {
25
30
  permissionChange?: boolean;
26
31
  migrationChange?: boolean;
27
32
  exportBreak?: boolean;
33
+ review?: PromoteReviewBinding;
28
34
  };
29
35
  }
36
+ /** How a review binding is written on the command line: `<base>:<digest>`, `-` for no base. */
37
+ export declare function reviewToken(review: PromoteReviewBinding): string;
38
+ /** `reviewToken` read back, or null when it is not one. The digest is hex, so the last `:` splits it. */
39
+ export declare function parseReviewToken(token: string): PromoteReviewBinding | null;
30
40
  /** The listing a refused (or acknowledged) export break carries, one line per affected app. */
31
41
  export declare function exportBreakLines(b: ExportBreaks): string[];
32
42
  /** The listing a refusal prints, and how to answer it once it has been read. */
@@ -53,6 +63,11 @@ export declare function explainRefusal(opts: PromoteOptions, refused: {
53
63
  * "none" is never the answer, because the gate has just said there is a change.
54
64
  */
55
65
  export declare function formatRegistryDiff(from: PermissionRegistry | null, to: PermissionRegistry | null, digestMoved: boolean): string[];
66
+ /**
67
+ * A module id or migration version on the listing's header line: bare when it is a plain id, as
68
+ * every id the push boundary admits is, and `quoted` otherwise, so nothing in it can break the line.
69
+ */
70
+ export declare const idOf: (text: string) => string;
56
71
  /**
57
72
  * The migrations a promote adds, each with its SQL. `null` is a version whose manifest carries
58
73
  * none. `digestMoved` is whether the gate refused on the migration digest.
@@ -1 +1 @@
1
- {"version":3,"file":"promote.d.ts","sourceRoot":"","sources":["../src/promote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAKL,KAAK,aAAa,EAElB,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,oCAAoC,CAAC;AAI5C,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAChG;AAED,+FAA+F;AAC/F,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAQ1D;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAE1D;AAED,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;AAEzD,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAkC1E;AAOD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACnD,OAAO,CAAC,MAAM,EAAE,CAAC,CA8BnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,EAAE,EAAE,EAAE,kBAAkB,GAAG,IAAI,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,EAAE,CA0BjI;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,EAAE,CAyB9F"}
1
+ {"version":3,"file":"promote.d.ts","sourceRoot":"","sources":["../src/promote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAUL,KAAK,aAAa,EAElB,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAC;AAI5C,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,oBAAoB,CAAA;KAAE,CAAC;CAC/H;AAED,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAEhE;AAED,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAM3E;AAED,+FAA+F;AAC/F,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAQ1D;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAE1D;AAED,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;AAEzD,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqC1E;AAOD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACnD,OAAO,CAAC,MAAM,EAAE,CAAC,CAmCnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,EAAE,EAAE,EAAE,kBAAkB,GAAG,IAAI,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,EAAE,CA0BjI;AAYD;;;GAGG;AACH,eAAO,MAAM,IAAI,SAAU,MAAM,KAAG,MAAqD,CAAC;AAwB1F;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,EAAE,CA8C9F"}