fix(audio-engine): single persistence store for device settings (TLC Part II §4)

The device config was persisted in TWO stores — the main process's
file-backed settings AND localStorage['slopsmith-audio-device'] — merged on
load by newest-savedAt. A main-side migration/reset left stale localStorage
that could win the timestamp race and resurrect wiped settings, and a device
re-save from either path re-persisted mute flags captured at that moment,
interleaving with the (now-arbitrated) runtime mute writers.

The file store is now the only write target. localStorage is treated as a
one-time migration source: a strictly-newer browser copy is imported into
the file store, then the key is deleted either way — after the first load
the file is the single source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-14 03:02:18 +02:00
co-authored by Claude Fable 5
parent 88f881dd1e
commit f0340ed425
+21 -6
View File
@@ -210,11 +210,14 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
} }
function saveDeviceSettings(settings = captureDeviceSettings()) { function saveDeviceSettings(settings = captureDeviceSettings()) {
// Single persistence store (TLC Part II §4): the file-backed settings
// are the only writer target. The old parallel localStorage copy meant
// a main-side migration/reset could lose the timestamp race against a
// stale browser copy and resurrect wiped settings.
const snapshot = { const snapshot = {
...cloneDeviceSettings(settings), ...cloneDeviceSettings(settings),
savedAt: Date.now(), savedAt: Date.now(),
}; };
try { localStorage.setItem('slopsmith-audio-device', JSON.stringify(snapshot)); } catch (_) {}
pendingDeviceSave = pendingDeviceSave pendingDeviceSave = pendingDeviceSave
.catch(() => null) .catch(() => null)
.then(() => { .then(() => {
@@ -255,17 +258,29 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
} catch (e) { } catch (e) {
console.warn('[audio-engine] Failed to load file-backed device settings:', e); console.warn('[audio-engine] Failed to load file-backed device settings:', e);
} }
// Migration only (TLC Part II §4): 'slopsmith-audio-device' was a
// second store racing the file on savedAt. Import a strictly-newer
// browser copy into the file store ONCE, then delete the key either
// way — after this the file is the single source of truth.
let browserSettings = null; let browserSettings = null;
try { try {
const raw = localStorage.getItem('slopsmith-audio-device'); const raw = localStorage.getItem('slopsmith-audio-device');
browserSettings = normalizeDeviceSettings(raw ? JSON.parse(raw) : null); browserSettings = normalizeDeviceSettings(raw ? JSON.parse(raw) : null);
} catch { browserSettings = null; } } catch { browserSettings = null; }
if (fileSettings && browserSettings) { if (browserSettings !== null) {
return getDeviceSettingsSavedAt(browserSettings) > getDeviceSettingsSavedAt(fileSettings) const browserNewer = !fileSettings
? browserSettings || getDeviceSettingsSavedAt(browserSettings) > getDeviceSettingsSavedAt(fileSettings);
: fileSettings; if (browserNewer) {
try {
if (typeof api.saveDeviceSettings === 'function') await api.saveDeviceSettings(browserSettings);
} catch (e) {
console.warn('[audio-engine] device-settings migration save failed:', e);
}
}
try { localStorage.removeItem('slopsmith-audio-device'); } catch (_) {}
if (browserNewer) return browserSettings;
} }
return fileSettings || browserSettings; return fileSettings;
} }
function hasSettingValue(value) { function hasSettingValue(value) {