Lumi/plugins/lumi_transcription/backend/config/revision_store.js
2026-07-22 11:01:49 +02:00

53 lines
3.0 KiB
JavaScript

const ALLOWED_KEYS = new Set([
"selected_model_id", "fallback_order", "decode_interval_ms", "silence_finalize_ms",
"rolling_context_ms", "caption_max_chars", "minimum_display_ms", "auto_start_stream",
"offline_authorization_severity", "diagnostic_caption_text", "tracks"
]);
class RevisionStore {
constructor(db, options = {}) { this.db = db; this.now = options.now || Date.now; this.migrate(); }
migrate() {
this.db.exec(`CREATE TABLE IF NOT EXISTS transcription_settings (
key TEXT PRIMARY KEY, value_json TEXT NOT NULL, revision INTEGER NOT NULL,
actor_id TEXT NOT NULL, updated_at INTEGER NOT NULL
);`);
}
list() {
return Object.fromEntries(this.db.prepare("SELECT * FROM transcription_settings ORDER BY key").all().map((row) => [row.key, decode(row)]));
}
apply(changes, actorId) {
if (!Array.isArray(changes) || !changes.length || changes.length > 50) throw new Error("One to fifty field changes are required.");
const normalized = changes.map(validateChange);
const applied = [];
const conflicts = [];
this.db.transaction(() => {
for (const change of normalized) {
const current = this.db.prepare("SELECT * FROM transcription_settings WHERE key = ?").get(change.key);
const currentRevision = current?.revision || 0;
if (change.base_revision !== currentRevision) {
conflicts.push({ key: change.key, local_value: change.value, local_base_revision: change.base_revision, server: current ? decode(current) : { value: null, revision: 0, actor_id: null, updated_at: null } });
continue;
}
const revision = currentRevision + 1;
const updatedAt = this.now();
this.db.prepare("INSERT INTO transcription_settings (key, value_json, revision, actor_id, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json, revision = excluded.revision, actor_id = excluded.actor_id, updated_at = excluded.updated_at")
.run(change.key, JSON.stringify(change.value), revision, String(actorId || "unknown"), updatedAt);
applied.push({ key: change.key, value: change.value, revision, actor_id: String(actorId || "unknown"), updated_at: updatedAt });
}
})();
return { applied, conflicts, current: this.list() };
}
}
function validateChange(change) {
if (!change || !ALLOWED_KEYS.has(change.key)) throw new Error(`Setting key ${change?.key || "(missing)"} is not allowed.`);
const baseRevision = Number(change.base_revision);
if (!Number.isInteger(baseRevision) || baseRevision < 0) throw new Error("A non-negative base revision is required for every changed field.");
const encoded = JSON.stringify(change.value);
if (encoded == null || Buffer.byteLength(encoded) > 64 * 1024) throw new Error("Setting value is too large.");
return { key: change.key, value: change.value, base_revision: baseRevision };
}
function decode(row) { return { value: JSON.parse(row.value_json), revision: row.revision, actor_id: row.actor_id, updated_at: row.updated_at }; }
module.exports = { RevisionStore, ALLOWED_KEYS };