Harden state persistence: atomic writes + backup fallback
- Atomic save: write to .tmp file then rename (prevents corruption if container is killed mid-write) - Backup: .bak copy created on successful load, used as fallback if main file is corrupted - Startup log shows loaded keys (verifies favorites survived) Ensures radio_favorites and radio_volumes survive container updates, crashes, and forced restarts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
262cc9f213
commit
9aefc3d470
1 changed files with 23 additions and 7 deletions
|
|
@ -3,25 +3,41 @@ import path from 'node:path';
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR ?? '/data';
|
const DATA_DIR = process.env.DATA_DIR ?? '/data';
|
||||||
const stateFile = path.join(DATA_DIR, 'hub-state.json');
|
const stateFile = path.join(DATA_DIR, 'hub-state.json');
|
||||||
|
const backupFile = stateFile + '.bak';
|
||||||
|
const tmpFile = stateFile + '.tmp';
|
||||||
|
|
||||||
let state: Record<string, any> = {};
|
let state: Record<string, any> = {};
|
||||||
|
|
||||||
export function loadState(): void {
|
export function loadState(): void {
|
||||||
try {
|
// Try main file first, fall back to backup if corrupted
|
||||||
if (fs.existsSync(stateFile)) {
|
for (const file of [stateFile, backupFile]) {
|
||||||
state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
try {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
state = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||||
|
const keys = Object.keys(state);
|
||||||
|
console.log(`[State] Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`);
|
||||||
|
// Create backup on successful load from main file
|
||||||
|
if (file === stateFile) {
|
||||||
|
try { fs.copyFileSync(stateFile, backupFile); } catch {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[State] Failed to load ${path.basename(file)}:`, e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load state:', e);
|
|
||||||
}
|
}
|
||||||
|
console.log('[State] No existing state found, starting fresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveState(): void {
|
export function saveState(): void {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
||||||
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
// Atomic write: write to tmp file, then rename
|
||||||
|
// rename() is atomic on POSIX filesystems, prevents corruption on crash
|
||||||
|
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2));
|
||||||
|
fs.renameSync(tmpFile, stateFile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save state:', e);
|
console.error('[State] Failed to save:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue