@objectstack/core 17.0.0-rc.1 → 17.0.0-rc.2
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.
- package/CHANGELOG.md +248 -0
- package/dist/index.cjs +453 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +234 -3
- package/dist/index.d.ts +234 -3
- package/dist/index.js +446 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1851,17 +1851,51 @@ var ObjectKernel = class {
|
|
|
1851
1851
|
assertInitServiceRequirements(plugin, (name) => this.hasAnyService(name));
|
|
1852
1852
|
this.currentlyInitializing = plugin.name;
|
|
1853
1853
|
try {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
});
|
|
1860
|
-
await Promise.race([initPromise, timeoutPromise]);
|
|
1854
|
+
await this.raceStartupTimeout(
|
|
1855
|
+
plugin.init(this.context),
|
|
1856
|
+
timeout,
|
|
1857
|
+
`Plugin ${plugin.name} init timeout after ${timeout}ms`
|
|
1858
|
+
);
|
|
1861
1859
|
} finally {
|
|
1862
1860
|
this.currentlyInitializing = void 0;
|
|
1863
1861
|
}
|
|
1864
1862
|
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Race a plugin lifecycle hook against its startup-timeout guard, and
|
|
1865
|
+
* reclaim the guard the moment the race settles (#4813).
|
|
1866
|
+
*
|
|
1867
|
+
* The guard used to be armed and then abandoned: when the plugin won the
|
|
1868
|
+
* race, its `setTimeout` stayed ref'd in the event loop for the full
|
|
1869
|
+
* `startupTimeout`, so every process idled that long after its work was
|
|
1870
|
+
* done. One `os migrate` finished in 3s and then sat for 120s
|
|
1871
|
+
* (`ObjectQLPlugin.startupTimeout`), held open by 8 orphaned guards — one
|
|
1872
|
+
* per init plus one per start.
|
|
1873
|
+
*
|
|
1874
|
+
* Clearing on settle rather than `unref()`-ing at arm time is deliberate.
|
|
1875
|
+
* An unref'd guard also stops pinning the loop, but it stops being a guard
|
|
1876
|
+
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
1877
|
+
* Node exits before the timer can fire and the timeout is never reported.
|
|
1878
|
+
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
1879
|
+
* which is what `clearTimeout` in a `finally` expresses.
|
|
1880
|
+
*
|
|
1881
|
+
* `operation` is widened to `T | PromiseLike<T>` because the Plugin
|
|
1882
|
+
* contract permits a synchronous hook (`init`/`start` return
|
|
1883
|
+
* `void | Promise<void>`); such a hook wins the race immediately and the
|
|
1884
|
+
* guard is reclaimed on the same turn.
|
|
1885
|
+
*/
|
|
1886
|
+
async raceStartupTimeout(operation, timeout, message) {
|
|
1887
|
+
let guard;
|
|
1888
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1889
|
+
guard = setTimeout(() => {
|
|
1890
|
+
reject(new Error(message));
|
|
1891
|
+
}, timeout);
|
|
1892
|
+
});
|
|
1893
|
+
try {
|
|
1894
|
+
return await Promise.race([operation, timeoutPromise]);
|
|
1895
|
+
} finally {
|
|
1896
|
+
clearTimeout(guard);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1865
1899
|
/**
|
|
1866
1900
|
* Whether a service is resolvable on this kernel right now — direct
|
|
1867
1901
|
* registration or a loader-registered factory. Backs the init-service
|
|
@@ -1887,13 +1921,11 @@ var ObjectKernel = class {
|
|
|
1887
1921
|
const startTime = Date.now();
|
|
1888
1922
|
this.logger.debug(`Start: ${plugin.name}`, { plugin: plugin.name });
|
|
1889
1923
|
try {
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
});
|
|
1896
|
-
await Promise.race([startPromise, timeoutPromise]);
|
|
1924
|
+
await this.raceStartupTimeout(
|
|
1925
|
+
plugin.start(this.context),
|
|
1926
|
+
timeout,
|
|
1927
|
+
`Plugin ${plugin.name} start timeout after ${timeout}ms`
|
|
1928
|
+
);
|
|
1897
1929
|
const duration = Date.now() - startTime;
|
|
1898
1930
|
this.startedPlugins.add(plugin.name);
|
|
1899
1931
|
this.pluginStartTimes.set(plugin.name, duration);
|
|
@@ -4955,6 +4987,395 @@ async function bulkWrite(rows, opts) {
|
|
|
4955
4987
|
return results;
|
|
4956
4988
|
}
|
|
4957
4989
|
|
|
4990
|
+
// src/utils/migration-journal.ts
|
|
4991
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
4992
|
+
import {
|
|
4993
|
+
MIGRATION_JOURNAL_OBJECT
|
|
4994
|
+
} from "@objectstack/spec/system";
|
|
4995
|
+
var SYSTEM_CTX = { isSystem: true };
|
|
4996
|
+
var DEFAULT_CHUNK_SIZE = 200;
|
|
4997
|
+
function engineCanRollBack(engine) {
|
|
4998
|
+
const e = engine;
|
|
4999
|
+
if (typeof e?.transaction !== "function") return false;
|
|
5000
|
+
const defaultDriverName = e.getDefaultDriverName?.();
|
|
5001
|
+
const defaultDriver = defaultDriverName ? e.getDriverByName?.(defaultDriverName) : void 0;
|
|
5002
|
+
return !defaultDriver || typeof defaultDriver.beginTransaction === "function";
|
|
5003
|
+
}
|
|
5004
|
+
var MigrationPlanRegistry = class {
|
|
5005
|
+
constructor() {
|
|
5006
|
+
this.plans = /* @__PURE__ */ new Map();
|
|
5007
|
+
}
|
|
5008
|
+
register(plan) {
|
|
5009
|
+
this.plans.set(plan.id, plan);
|
|
5010
|
+
}
|
|
5011
|
+
get(planId) {
|
|
5012
|
+
return this.plans.get(planId);
|
|
5013
|
+
}
|
|
5014
|
+
list() {
|
|
5015
|
+
return [...this.plans.values()];
|
|
5016
|
+
}
|
|
5017
|
+
};
|
|
5018
|
+
var MigrationJournalRefusal = class extends Error {
|
|
5019
|
+
constructor(code, message) {
|
|
5020
|
+
super(message);
|
|
5021
|
+
this.name = "MigrationJournalRefusal";
|
|
5022
|
+
this.code = code;
|
|
5023
|
+
}
|
|
5024
|
+
};
|
|
5025
|
+
function planChunks(plan, rowCounts, chunkSize = plan.chunkSize ?? DEFAULT_CHUNK_SIZE) {
|
|
5026
|
+
const size = Math.max(1, chunkSize);
|
|
5027
|
+
const chunks = [];
|
|
5028
|
+
plan.steps.forEach((step, stepIndex) => {
|
|
5029
|
+
const total = rowCounts[stepIndex] ?? 0;
|
|
5030
|
+
for (let offset = 0; offset < total; offset += size) {
|
|
5031
|
+
chunks.push({
|
|
5032
|
+
index: chunks.length,
|
|
5033
|
+
stepIndex,
|
|
5034
|
+
stepName: step.name,
|
|
5035
|
+
offset,
|
|
5036
|
+
length: Math.min(size, total - offset)
|
|
5037
|
+
});
|
|
5038
|
+
}
|
|
5039
|
+
});
|
|
5040
|
+
return chunks;
|
|
5041
|
+
}
|
|
5042
|
+
function hashMigrationPlan(plan, chunks) {
|
|
5043
|
+
const shape = JSON.stringify({
|
|
5044
|
+
id: plan.id,
|
|
5045
|
+
steps: plan.steps.map((s) => s.name),
|
|
5046
|
+
chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
|
|
5047
|
+
});
|
|
5048
|
+
return createHash2("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
|
|
5049
|
+
}
|
|
5050
|
+
async function appendEvent(engine, event, execContext) {
|
|
5051
|
+
await engine.insert(
|
|
5052
|
+
MIGRATION_JOURNAL_OBJECT,
|
|
5053
|
+
{ ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
|
|
5054
|
+
{ context: execContext ?? { ...SYSTEM_CTX } }
|
|
5055
|
+
);
|
|
5056
|
+
}
|
|
5057
|
+
async function readRunJournal(engine, runId) {
|
|
5058
|
+
const rows = await engine.find(
|
|
5059
|
+
MIGRATION_JOURNAL_OBJECT,
|
|
5060
|
+
{ where: { run_id: runId } },
|
|
5061
|
+
{ context: { ...SYSTEM_CTX } }
|
|
5062
|
+
);
|
|
5063
|
+
return [...rows ?? []].sort((a, b) => Number(a.seq) - Number(b.seq));
|
|
5064
|
+
}
|
|
5065
|
+
function chunkSetOf(events, kind) {
|
|
5066
|
+
const out = /* @__PURE__ */ new Set();
|
|
5067
|
+
for (const e of events) {
|
|
5068
|
+
if (e.kind === kind && typeof e.chunk_index === "number") out.add(e.chunk_index);
|
|
5069
|
+
}
|
|
5070
|
+
return out;
|
|
5071
|
+
}
|
|
5072
|
+
async function findInterruptedRuns(engine) {
|
|
5073
|
+
const started = await engine.find(
|
|
5074
|
+
MIGRATION_JOURNAL_OBJECT,
|
|
5075
|
+
{ where: { kind: "run_started" } },
|
|
5076
|
+
{ context: { ...SYSTEM_CTX } }
|
|
5077
|
+
);
|
|
5078
|
+
const out = [];
|
|
5079
|
+
for (const start of started ?? []) {
|
|
5080
|
+
const events = await readRunJournal(engine, start.run_id);
|
|
5081
|
+
if (events.some((e) => e.kind === "run_done")) continue;
|
|
5082
|
+
const committed = chunkSetOf(events, "chunk_done");
|
|
5083
|
+
const compensated = chunkSetOf(events, "compensated");
|
|
5084
|
+
const outstanding = [...committed].filter((i) => !compensated.has(i));
|
|
5085
|
+
if (events.some((e) => e.kind === "run_failed") && outstanding.length === 0) continue;
|
|
5086
|
+
const unknown = [...chunkSetOf(events, "chunk_started")].filter((i) => !committed.has(i));
|
|
5087
|
+
let planId = start.run_id;
|
|
5088
|
+
try {
|
|
5089
|
+
planId = start.detail ? JSON.parse(start.detail).planId ?? start.run_id : start.run_id;
|
|
5090
|
+
} catch {
|
|
5091
|
+
}
|
|
5092
|
+
out.push({
|
|
5093
|
+
runId: start.run_id,
|
|
5094
|
+
planId,
|
|
5095
|
+
planHash: start.plan_hash ?? "",
|
|
5096
|
+
migrationId: start.migration_id,
|
|
5097
|
+
startedAt: start.created_at,
|
|
5098
|
+
committedChunks: [...committed].sort((a, b) => a - b),
|
|
5099
|
+
unknownChunks: unknown.sort((a, b) => a - b),
|
|
5100
|
+
compensatedChunks: [...compensated].sort((a, b) => a - b)
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
return out;
|
|
5104
|
+
}
|
|
5105
|
+
async function loadPlan(engine, plan, chunkSize) {
|
|
5106
|
+
const rowsByStep = [];
|
|
5107
|
+
for (const step of plan.steps) rowsByStep.push(await step.load(engine) ?? []);
|
|
5108
|
+
const chunks = planChunks(plan, rowsByStep.map((r) => r.length), chunkSize ?? plan.chunkSize);
|
|
5109
|
+
return { chunks, planHash: hashMigrationPlan(plan, chunks), rowsByStep };
|
|
5110
|
+
}
|
|
5111
|
+
async function runMigrationJournal(engine, plan, options = {}) {
|
|
5112
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
5113
|
+
if (!engineCanRollBack(engine)) {
|
|
5114
|
+
throw new MigrationJournalRefusal(
|
|
5115
|
+
"NOT_IMPLEMENTED",
|
|
5116
|
+
`Migration plan '${plan.id}' requires engine transaction support; this runtime cannot roll back. The journal's chunk_done markers would not mean "committed", so the run is refused rather than started.`
|
|
5117
|
+
);
|
|
5118
|
+
}
|
|
5119
|
+
const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
|
|
5120
|
+
const resuming = Boolean(options.runId);
|
|
5121
|
+
const runId = options.runId ?? randomUUID();
|
|
5122
|
+
let events = [];
|
|
5123
|
+
let seq = 0;
|
|
5124
|
+
let committed = /* @__PURE__ */ new Set();
|
|
5125
|
+
let compensated = /* @__PURE__ */ new Set();
|
|
5126
|
+
const attemptsByChunk = /* @__PURE__ */ new Map();
|
|
5127
|
+
if (resuming) {
|
|
5128
|
+
events = await readRunJournal(engine, runId);
|
|
5129
|
+
if (events.length === 0) {
|
|
5130
|
+
throw new MigrationJournalRefusal("NO_SUCH_RUN", `No journal rows for run '${runId}'.`);
|
|
5131
|
+
}
|
|
5132
|
+
const start = events.find((e) => e.kind === "run_started");
|
|
5133
|
+
if (start?.plan_hash && start.plan_hash !== planHash) {
|
|
5134
|
+
throw new MigrationJournalRefusal(
|
|
5135
|
+
"PLAN_CHANGED",
|
|
5136
|
+
`Refusing to resume run '${runId}': plan hash ${planHash} does not match the journal's ${start.plan_hash}. The chunk boundaries recorded in the journal describe a different plan.`
|
|
5137
|
+
);
|
|
5138
|
+
}
|
|
5139
|
+
if (events.some((e) => e.kind === "run_done")) {
|
|
5140
|
+
return {
|
|
5141
|
+
runId,
|
|
5142
|
+
status: "completed",
|
|
5143
|
+
chunksTotal: chunks.length,
|
|
5144
|
+
chunksCommitted: chunkSetOf(events, "chunk_done").size,
|
|
5145
|
+
chunksCompensated: chunkSetOf(events, "compensated").size,
|
|
5146
|
+
planHash
|
|
5147
|
+
};
|
|
5148
|
+
}
|
|
5149
|
+
seq = events.reduce((m, e) => Math.max(m, Number(e.seq) + 1), 0);
|
|
5150
|
+
committed = chunkSetOf(events, "chunk_done");
|
|
5151
|
+
compensated = chunkSetOf(events, "compensated");
|
|
5152
|
+
for (const e of events) {
|
|
5153
|
+
if (e.kind === "chunk_started" && typeof e.chunk_index === "number") {
|
|
5154
|
+
attemptsByChunk.set(e.chunk_index, (attemptsByChunk.get(e.chunk_index) ?? 0) + 1);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
for (const step of plan.steps) {
|
|
5159
|
+
if (!step.preflight) continue;
|
|
5160
|
+
try {
|
|
5161
|
+
await step.preflight(engine);
|
|
5162
|
+
} catch (err) {
|
|
5163
|
+
throw new MigrationJournalRefusal(
|
|
5164
|
+
"PREFLIGHT_FAILED",
|
|
5165
|
+
`Migration plan '${plan.id}' refused: preflight for step '${step.name}' failed: ${errText(err)}`
|
|
5166
|
+
);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
if (plan.onCrash === "compensate") {
|
|
5170
|
+
const missing = plan.steps.filter((s) => !s.compensate).map((s) => s.name);
|
|
5171
|
+
if (missing.length > 0) {
|
|
5172
|
+
throw new MigrationJournalRefusal(
|
|
5173
|
+
"NOT_COMPENSABLE",
|
|
5174
|
+
`Migration plan '${plan.id}' declares onCrash: 'compensate' but step(s) ${missing.join(", ")} declare no compensate().`
|
|
5175
|
+
);
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
const rowsOf = (c) => rowsByStep[c.stepIndex].slice(c.offset, c.offset + c.length);
|
|
5179
|
+
const next = () => seq++;
|
|
5180
|
+
if (!resuming) {
|
|
5181
|
+
await appendEvent(engine, {
|
|
5182
|
+
run_id: runId,
|
|
5183
|
+
seq: next(),
|
|
5184
|
+
kind: "run_started",
|
|
5185
|
+
plan_hash: planHash,
|
|
5186
|
+
migration_id: plan.migrationId,
|
|
5187
|
+
created_at: now(),
|
|
5188
|
+
detail: JSON.stringify({
|
|
5189
|
+
planId: plan.id,
|
|
5190
|
+
onCrash: plan.onCrash ?? "resume",
|
|
5191
|
+
chunks: chunks.map((c) => ({ i: c.index, step: c.stepName, offset: c.offset, length: c.length }))
|
|
5192
|
+
})
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
if (resuming && plan.onCrash === "compensate") {
|
|
5196
|
+
return await unwind(engine, plan, {
|
|
5197
|
+
runId,
|
|
5198
|
+
planHash,
|
|
5199
|
+
chunks,
|
|
5200
|
+
rowsOf,
|
|
5201
|
+
next,
|
|
5202
|
+
now,
|
|
5203
|
+
committed,
|
|
5204
|
+
compensated,
|
|
5205
|
+
chunksTotal: chunks.length,
|
|
5206
|
+
cause: new Error(`run '${runId}' rediscovered after interruption; plan policy is compensate`)
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
for (const chunk of chunks) {
|
|
5210
|
+
if (committed.has(chunk.index)) continue;
|
|
5211
|
+
const attempt = (attemptsByChunk.get(chunk.index) ?? 0) + 1;
|
|
5212
|
+
attemptsByChunk.set(chunk.index, attempt);
|
|
5213
|
+
const step = plan.steps[chunk.stepIndex];
|
|
5214
|
+
const rows = rowsOf(chunk);
|
|
5215
|
+
await appendEvent(engine, {
|
|
5216
|
+
run_id: runId,
|
|
5217
|
+
seq: next(),
|
|
5218
|
+
kind: "chunk_started",
|
|
5219
|
+
chunk_index: chunk.index,
|
|
5220
|
+
attempt,
|
|
5221
|
+
migration_id: plan.migrationId,
|
|
5222
|
+
created_at: now()
|
|
5223
|
+
});
|
|
5224
|
+
try {
|
|
5225
|
+
await engine.transaction(async (trxCtx) => {
|
|
5226
|
+
await step.forward(rows, { runId, chunkIndex: chunk.index, attempt, context: trxCtx }, engine);
|
|
5227
|
+
await appendEvent(
|
|
5228
|
+
engine,
|
|
5229
|
+
{
|
|
5230
|
+
run_id: runId,
|
|
5231
|
+
seq: next(),
|
|
5232
|
+
kind: "chunk_done",
|
|
5233
|
+
chunk_index: chunk.index,
|
|
5234
|
+
attempt,
|
|
5235
|
+
migration_id: plan.migrationId,
|
|
5236
|
+
created_at: now()
|
|
5237
|
+
},
|
|
5238
|
+
trxCtx
|
|
5239
|
+
);
|
|
5240
|
+
}, { ...SYSTEM_CTX });
|
|
5241
|
+
committed.add(chunk.index);
|
|
5242
|
+
} catch (err) {
|
|
5243
|
+
return await unwind(engine, plan, {
|
|
5244
|
+
runId,
|
|
5245
|
+
planHash,
|
|
5246
|
+
chunks,
|
|
5247
|
+
rowsOf,
|
|
5248
|
+
next,
|
|
5249
|
+
now,
|
|
5250
|
+
committed,
|
|
5251
|
+
compensated,
|
|
5252
|
+
chunksTotal: chunks.length,
|
|
5253
|
+
cause: err
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
await appendEvent(engine, {
|
|
5258
|
+
run_id: runId,
|
|
5259
|
+
seq: next(),
|
|
5260
|
+
kind: "run_done",
|
|
5261
|
+
migration_id: plan.migrationId,
|
|
5262
|
+
created_at: now()
|
|
5263
|
+
});
|
|
5264
|
+
return {
|
|
5265
|
+
runId,
|
|
5266
|
+
status: "completed",
|
|
5267
|
+
chunksTotal: chunks.length,
|
|
5268
|
+
chunksCommitted: committed.size,
|
|
5269
|
+
chunksCompensated: compensated.size,
|
|
5270
|
+
planHash
|
|
5271
|
+
};
|
|
5272
|
+
}
|
|
5273
|
+
async function unwind(engine, plan, a) {
|
|
5274
|
+
const order = [...a.committed].sort((x, y) => y - x);
|
|
5275
|
+
for (const index of order) {
|
|
5276
|
+
if (a.compensated.has(index)) continue;
|
|
5277
|
+
const chunk = a.chunks[index];
|
|
5278
|
+
const step = plan.steps[chunk.stepIndex];
|
|
5279
|
+
if (!step.compensate) {
|
|
5280
|
+
await appendEvent(engine, {
|
|
5281
|
+
run_id: a.runId,
|
|
5282
|
+
seq: a.next(),
|
|
5283
|
+
kind: "run_failed",
|
|
5284
|
+
chunk_index: index,
|
|
5285
|
+
migration_id: plan.migrationId,
|
|
5286
|
+
created_at: a.now(),
|
|
5287
|
+
detail: JSON.stringify({
|
|
5288
|
+
phase: "compensate",
|
|
5289
|
+
reason: "step declares no compensate()",
|
|
5290
|
+
step: step.name,
|
|
5291
|
+
cause: errText(a.cause)
|
|
5292
|
+
})
|
|
5293
|
+
});
|
|
5294
|
+
return {
|
|
5295
|
+
runId: a.runId,
|
|
5296
|
+
status: "failed",
|
|
5297
|
+
chunksTotal: a.chunksTotal,
|
|
5298
|
+
chunksCommitted: a.committed.size,
|
|
5299
|
+
chunksCompensated: a.compensated.size,
|
|
5300
|
+
planHash: a.planHash,
|
|
5301
|
+
error: a.cause
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
const attempt = 1;
|
|
5305
|
+
try {
|
|
5306
|
+
await engine.transaction(async (trxCtx) => {
|
|
5307
|
+
await step.compensate(a.rowsOf(chunk), { runId: a.runId, chunkIndex: index, attempt, context: trxCtx }, engine);
|
|
5308
|
+
await appendEvent(
|
|
5309
|
+
engine,
|
|
5310
|
+
{
|
|
5311
|
+
run_id: a.runId,
|
|
5312
|
+
seq: a.next(),
|
|
5313
|
+
kind: "compensated",
|
|
5314
|
+
chunk_index: index,
|
|
5315
|
+
attempt,
|
|
5316
|
+
migration_id: plan.migrationId,
|
|
5317
|
+
created_at: a.now()
|
|
5318
|
+
},
|
|
5319
|
+
trxCtx
|
|
5320
|
+
);
|
|
5321
|
+
}, { ...SYSTEM_CTX });
|
|
5322
|
+
a.compensated.add(index);
|
|
5323
|
+
} catch (err) {
|
|
5324
|
+
await appendEvent(engine, {
|
|
5325
|
+
run_id: a.runId,
|
|
5326
|
+
seq: a.next(),
|
|
5327
|
+
kind: "run_failed",
|
|
5328
|
+
chunk_index: index,
|
|
5329
|
+
migration_id: plan.migrationId,
|
|
5330
|
+
created_at: a.now(),
|
|
5331
|
+
detail: JSON.stringify({
|
|
5332
|
+
phase: "compensate",
|
|
5333
|
+
step: step.name,
|
|
5334
|
+
error: errText(err),
|
|
5335
|
+
cause: errText(a.cause)
|
|
5336
|
+
})
|
|
5337
|
+
});
|
|
5338
|
+
return {
|
|
5339
|
+
runId: a.runId,
|
|
5340
|
+
status: "failed",
|
|
5341
|
+
chunksTotal: a.chunksTotal,
|
|
5342
|
+
chunksCommitted: a.committed.size,
|
|
5343
|
+
chunksCompensated: a.compensated.size,
|
|
5344
|
+
planHash: a.planHash,
|
|
5345
|
+
error: err
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
await appendEvent(engine, {
|
|
5350
|
+
run_id: a.runId,
|
|
5351
|
+
seq: a.next(),
|
|
5352
|
+
kind: "run_failed",
|
|
5353
|
+
migration_id: plan.migrationId,
|
|
5354
|
+
created_at: a.now(),
|
|
5355
|
+
detail: JSON.stringify({ phase: "forward", error: errText(a.cause), compensated: [...a.compensated].sort((x, y) => x - y) })
|
|
5356
|
+
});
|
|
5357
|
+
return {
|
|
5358
|
+
runId: a.runId,
|
|
5359
|
+
status: "compensated",
|
|
5360
|
+
chunksTotal: a.chunksTotal,
|
|
5361
|
+
chunksCommitted: a.committed.size,
|
|
5362
|
+
chunksCompensated: a.compensated.size,
|
|
5363
|
+
planHash: a.planHash,
|
|
5364
|
+
error: a.cause
|
|
5365
|
+
};
|
|
5366
|
+
}
|
|
5367
|
+
async function resumeMigrationJournal(engine, plan, runId, options = {}) {
|
|
5368
|
+
return runMigrationJournal(engine, plan, { ...options, runId });
|
|
5369
|
+
}
|
|
5370
|
+
function errText(err) {
|
|
5371
|
+
if (err instanceof Error) return err.message;
|
|
5372
|
+
try {
|
|
5373
|
+
return String(err);
|
|
5374
|
+
} catch {
|
|
5375
|
+
return "<unprintable error>";
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
|
|
4958
5379
|
// src/utils/filter-tokens.ts
|
|
4959
5380
|
import {
|
|
4960
5381
|
classifyFilterToken,
|
|
@@ -5404,7 +5825,7 @@ var PluginHealthMonitor = class {
|
|
|
5404
5825
|
};
|
|
5405
5826
|
|
|
5406
5827
|
// src/hot-reload.ts
|
|
5407
|
-
import { createHash as
|
|
5828
|
+
import { createHash as createHash3 } from "crypto";
|
|
5408
5829
|
var generateUUID = () => {
|
|
5409
5830
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
5410
5831
|
return crypto.randomUUID();
|
|
@@ -5499,7 +5920,7 @@ var PluginStateManager = class {
|
|
|
5499
5920
|
*/
|
|
5500
5921
|
calculateChecksum(state) {
|
|
5501
5922
|
const stateStr = JSON.stringify(state);
|
|
5502
|
-
return
|
|
5923
|
+
return createHash3("sha256").update(stateStr).digest("hex");
|
|
5503
5924
|
}
|
|
5504
5925
|
/**
|
|
5505
5926
|
* Shutdown state manager
|
|
@@ -6085,6 +6506,8 @@ export {
|
|
|
6085
6506
|
DependencyResolver,
|
|
6086
6507
|
HotReloadManager,
|
|
6087
6508
|
LiteKernel,
|
|
6509
|
+
MigrationJournalRefusal,
|
|
6510
|
+
MigrationPlanRegistry,
|
|
6088
6511
|
NamespaceResolver,
|
|
6089
6512
|
ObjectKernel,
|
|
6090
6513
|
ObjectKernelBase,
|
|
@@ -6127,14 +6550,17 @@ export {
|
|
|
6127
6550
|
defaultIsTransientError,
|
|
6128
6551
|
derivePosture,
|
|
6129
6552
|
describeInitOrderFault,
|
|
6553
|
+
engineCanRollBack,
|
|
6130
6554
|
evaluateAuthGate,
|
|
6131
6555
|
extractApiKey,
|
|
6132
6556
|
filterTokenContextFrom,
|
|
6557
|
+
findInterruptedRuns,
|
|
6133
6558
|
generateApiKey,
|
|
6134
6559
|
generateEd25519KeyPair,
|
|
6135
6560
|
getEnv,
|
|
6136
6561
|
getMemoryUsage,
|
|
6137
6562
|
hashApiKey,
|
|
6563
|
+
hashMigrationPlan,
|
|
6138
6564
|
isAuthGateAllowlisted,
|
|
6139
6565
|
isExpired,
|
|
6140
6566
|
isGrantActive,
|
|
@@ -6143,8 +6569,10 @@ export {
|
|
|
6143
6569
|
nextUtcCalendarDay,
|
|
6144
6570
|
parseScopes,
|
|
6145
6571
|
parseSignature,
|
|
6572
|
+
planChunks,
|
|
6146
6573
|
postureVisibleRows,
|
|
6147
6574
|
readAuthoredTranslationLayer,
|
|
6575
|
+
readRunJournal,
|
|
6148
6576
|
resolveApiKeyPrincipal,
|
|
6149
6577
|
resolveAuthzContext,
|
|
6150
6578
|
resolveFilterToken,
|
|
@@ -6153,6 +6581,8 @@ export {
|
|
|
6153
6581
|
resolveLocalizationContext,
|
|
6154
6582
|
resolvePluginOrder,
|
|
6155
6583
|
resolveUserAuthzGrants,
|
|
6584
|
+
resumeMigrationJournal,
|
|
6585
|
+
runMigrationJournal,
|
|
6156
6586
|
safeExit,
|
|
6157
6587
|
shouldDenyAnonymous,
|
|
6158
6588
|
signPayload,
|