118 lines
5.4 KiB
JavaScript
118 lines
5.4 KiB
JavaScript
import { fnv1a64 } from "../worker/replay.mjs";
|
|
|
|
export const CONFIGURATION_BATCH_SCHEMA = "configuration-startup-batch.v1";
|
|
const MAX_ENTRIES = 512;
|
|
const TERMINAL = new Set(["ready", "blocked"]);
|
|
|
|
const clone = (value) => structuredClone(value);
|
|
const text = (value) => typeof value === "string" && value.length > 0;
|
|
|
|
function diagnostic(row) {
|
|
const blockedCode = row.startup_preflight === "blocked_unindexed_resource" || row.resource_status === "unindexed"
|
|
? "resource_unindexed"
|
|
: row.startup_preflight === "blocked_unregistered_kinematics"
|
|
? "kinematics_unregistered"
|
|
: row.unresolved_resources?.length
|
|
? "resource_unresolved"
|
|
: "resource_missing";
|
|
return {
|
|
status: "blocked",
|
|
code: blockedCode,
|
|
config_id: row.config_id,
|
|
entrypoint: row.path,
|
|
module: row.module ?? null,
|
|
startup_preflight: row.startup_preflight,
|
|
resource_status: row.resource_status,
|
|
missing_resources: [...(row.missing_resources ?? [])].sort(),
|
|
unresolved_resources: [...(row.unresolved_resources ?? [])].sort(),
|
|
};
|
|
}
|
|
|
|
function normalizeRow(row, index) {
|
|
if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("configuration_batch_row");
|
|
if (!text(row.config_id) || !text(row.path) || !text(row.startup_preflight) || !text(row.resource_status)) throw new Error("configuration_batch_fields");
|
|
const terminal = row.startup_preflight === "static_ready" ? "ready" : "blocked";
|
|
if (!TERMINAL.has(terminal) || row.status !== terminal) throw new Error("configuration_batch_status");
|
|
if (!Array.isArray(row.missing_resources) || !Array.isArray(row.unresolved_resources)) throw new Error("configuration_batch_diagnostics");
|
|
return {
|
|
config_id: row.config_id,
|
|
path: row.path,
|
|
module: row.module ?? null,
|
|
family: row.family ?? null,
|
|
source_id: row.source_id ?? row.config_id,
|
|
line: Number.isSafeInteger(row.line) ? row.line : null,
|
|
status: terminal,
|
|
startup_preflight: row.startup_preflight,
|
|
resource_status: row.resource_status,
|
|
resource_rows: Number.isSafeInteger(row.resource_rows) ? row.resource_rows : 0,
|
|
missing_resources: [...row.missing_resources].map(String).sort(),
|
|
unresolved_resources: [...row.unresolved_resources].map(String).sort(),
|
|
...(terminal === "blocked" ? { diagnostic: diagnostic({ ...row, status: terminal }) } : {}),
|
|
};
|
|
}
|
|
|
|
export class ConfigurationBatchRuntime {
|
|
constructor({ context_id = null } = {}) {
|
|
this.contextId = context_id;
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
this.state = { schema: CONFIGURATION_BATCH_SCHEMA, context_id: this.contextId, entrypoints: [], counts: { total: 0, ready: 0, blocked: 0, startup_preflight: {}, resource_status: {} }, diagnostics: [], last_error: null };
|
|
return this.export();
|
|
}
|
|
|
|
export() {
|
|
const unsigned = clone(this.state);
|
|
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
|
}
|
|
|
|
hash() { return this.export().state_hash; }
|
|
|
|
fail(code, detail = null) {
|
|
this.state.last_error = code;
|
|
return { status: "blocked", code, ...(detail ? { detail } : {}), state: this.export() };
|
|
}
|
|
|
|
load(entries, { context_id = this.contextId } = {}) {
|
|
if (!Array.isArray(entries) || entries.length === 0 || entries.length > MAX_ENTRIES) return this.fail("configuration_batch_capacity");
|
|
if (context_id !== null && !text(context_id)) return this.fail("configuration_batch_context");
|
|
const before = clone(this.state);
|
|
try {
|
|
const normalized = entries.map(normalizeRow);
|
|
const ids = new Set();
|
|
for (const row of normalized) {
|
|
if (ids.has(row.config_id)) throw new Error("configuration_batch_duplicate");
|
|
ids.add(row.config_id);
|
|
}
|
|
normalized.sort((left, right) => left.config_id.localeCompare(right.config_id));
|
|
const counts = { total: normalized.length, ready: normalized.filter((row) => row.status === "ready").length, blocked: normalized.filter((row) => row.status === "blocked").length, startup_preflight: {}, resource_status: {} };
|
|
for (const row of normalized) {
|
|
counts.startup_preflight[row.startup_preflight] = (counts.startup_preflight[row.startup_preflight] ?? 0) + 1;
|
|
counts.resource_status[row.resource_status] = (counts.resource_status[row.resource_status] ?? 0) + 1;
|
|
}
|
|
this.contextId = context_id;
|
|
this.state = { schema: CONFIGURATION_BATCH_SCHEMA, context_id: this.contextId, entrypoints: normalized, counts, diagnostics: normalized.filter((row) => row.status === "blocked").map((row) => row.diagnostic), last_error: null };
|
|
return { status: "verified", command: "CONFIGURATION_BATCH", state: this.export() };
|
|
} catch (error) {
|
|
this.state = before;
|
|
return this.fail(error.message);
|
|
}
|
|
}
|
|
|
|
restore(value) {
|
|
if (!value || value.schema !== CONFIGURATION_BATCH_SCHEMA || !Array.isArray(value.entrypoints)) throw new Error("configuration_batch_restore_schema");
|
|
const unsigned = clone(value); delete unsigned.state_hash;
|
|
if (value.state_hash !== fnv1a64(unsigned)) throw new Error("configuration_batch_restore_hash");
|
|
if (unsigned.entrypoints.length > MAX_ENTRIES) throw new Error("configuration_batch_restore_capacity");
|
|
this.state = unsigned; this.contextId = unsigned.context_id ?? this.contextId;
|
|
return this.export();
|
|
}
|
|
}
|
|
|
|
export function classifyConfigurationBatch(entries, options = {}) {
|
|
return new ConfigurationBatchRuntime(options).load(entries, options);
|
|
}
|
|
|
|
export { MAX_ENTRIES as CONFIGURATION_BATCH_CAPACITY };
|