fix: state persistence diagnostics + fallback write
- loadState: logs DATA_DIR path, writability check, lists files, shows radio_favorites count on load - saveState: read-back verification after atomic write, fallback to direct write if rename fails - /api/health: shows state diagnostics (file exists, file size, keys, favorites count in memory vs disk, lastSaveOk) - Helps diagnose why favorites are not persisting across deploys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fcf4ba86ce
commit
55f311c612
2 changed files with 80 additions and 6 deletions
|
|
@ -7,19 +7,45 @@ const backupFile = stateFile + '.bak';
|
|||
const tmpFile = stateFile + '.tmp';
|
||||
|
||||
let state: Record<string, any> = {};
|
||||
let lastSaveOk = false;
|
||||
|
||||
export function loadState(): void {
|
||||
console.log(`[State] DATA_DIR=${DATA_DIR}, stateFile=${stateFile}`);
|
||||
|
||||
// Check if DATA_DIR exists and is writable
|
||||
try {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const testFile = path.join(DATA_DIR, '.write-test');
|
||||
fs.writeFileSync(testFile, 'ok');
|
||||
fs.unlinkSync(testFile);
|
||||
console.log(`[State] DATA_DIR is writable`);
|
||||
} catch (e) {
|
||||
console.error(`[State] ⚠ DATA_DIR is NOT writable:`, e);
|
||||
}
|
||||
|
||||
// List existing files in DATA_DIR for diagnostics
|
||||
try {
|
||||
const files = fs.readdirSync(DATA_DIR);
|
||||
console.log(`[State] Files in DATA_DIR: ${files.join(', ') || '(empty)'}`);
|
||||
} catch (e) {
|
||||
console.error(`[State] Cannot list DATA_DIR:`, e);
|
||||
}
|
||||
|
||||
// Try main file first, fall back to backup if corrupted
|
||||
for (const file of [stateFile, backupFile]) {
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
state = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
const raw = fs.readFileSync(file, 'utf-8');
|
||||
state = JSON.parse(raw);
|
||||
const keys = Object.keys(state);
|
||||
console.log(`[State] Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`);
|
||||
const favCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0;
|
||||
console.log(`[State] ✓ Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`);
|
||||
console.log(`[State] radio_favorites: ${favCount} entries`);
|
||||
// Create backup on successful load from main file
|
||||
if (file === stateFile) {
|
||||
try { fs.copyFileSync(stateFile, backupFile); } catch {}
|
||||
}
|
||||
lastSaveOk = true;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -32,12 +58,29 @@ export function loadState(): void {
|
|||
export function saveState(): void {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
||||
const json = 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.writeFileSync(tmpFile, json);
|
||||
fs.renameSync(tmpFile, stateFile);
|
||||
// Verify write by reading back
|
||||
const verify = fs.readFileSync(stateFile, 'utf-8');
|
||||
if (verify.length !== json.length) {
|
||||
console.error(`[State] ⚠ Write verification FAILED: wrote ${json.length} bytes, read back ${verify.length}`);
|
||||
lastSaveOk = false;
|
||||
} else {
|
||||
lastSaveOk = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[State] Failed to save:', e);
|
||||
console.error('[State] ✗ Failed to save:', e);
|
||||
lastSaveOk = false;
|
||||
// Fallback: try direct write if rename failed
|
||||
try {
|
||||
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
||||
console.log('[State] Fallback direct write succeeded');
|
||||
lastSaveOk = true;
|
||||
} catch (e2) {
|
||||
console.error('[State] ✗ Fallback write also failed:', e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,3 +96,33 @@ export function setState(key: string, value: any): void {
|
|||
export function getFullState(): Record<string, any> {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
/** Diagnostic info for health endpoint */
|
||||
export function getStateDiag(): Record<string, any> {
|
||||
let fileExists = false;
|
||||
let fileSize = 0;
|
||||
let fileKeys: string[] = [];
|
||||
let fileFavCount = 0;
|
||||
try {
|
||||
if (fs.existsSync(stateFile)) {
|
||||
fileExists = true;
|
||||
const stat = fs.statSync(stateFile);
|
||||
fileSize = stat.size;
|
||||
const parsed = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
||||
fileKeys = Object.keys(parsed);
|
||||
fileFavCount = Array.isArray(parsed.radio_favorites) ? parsed.radio_favorites.length : 0;
|
||||
}
|
||||
} catch {}
|
||||
const memKeys = Object.keys(state);
|
||||
const memFavCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0;
|
||||
return {
|
||||
stateFile,
|
||||
fileExists,
|
||||
fileSize,
|
||||
fileKeys,
|
||||
fileFavCount,
|
||||
memoryKeys: memKeys,
|
||||
memoryFavCount: memFavCount,
|
||||
lastSaveOk,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue