fix(settings): sync across tabs; merge-on-write instead of clobbering
The settings atom was a load-time snapshot with no storage listener and wrote the whole blob, so two tabs silently reverted each other. It now re-reads on storage events and writes only the keys that changed relative to the previous value. Unit-tested. Fixes #42 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -473,19 +473,50 @@ export const getSettings = (): Settings => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setSettings = (settings: Settings) => {
|
||||
// Gitea #42 — merge-on-write. `next` is always this tab's whole-object view
|
||||
// (a shallow copy of what it last read, with one key changed — see
|
||||
// useSetSetting), which is stale the moment another tab has written since. Re-
|
||||
// reading the stored blob and reapplying only the keys that actually changed
|
||||
// relative to `previous` (this tab's own pre-update snapshot) means a change
|
||||
// this tab didn't make — e.g. a different setting toggled in another tab —
|
||||
// survives instead of being clobbered by this tab's stale snapshot of it.
|
||||
export const setSettings = (previous: Settings, next: Settings) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
const stored = getSettings();
|
||||
const merged: Settings = { ...stored };
|
||||
(Object.keys(next) as (keyof Settings)[]).forEach((key) => {
|
||||
if (next[key] !== previous[key]) {
|
||||
(merged as Record<keyof Settings, unknown>)[key] = next[key];
|
||||
}
|
||||
});
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
||||
} catch {
|
||||
/* quota */
|
||||
}
|
||||
};
|
||||
|
||||
const baseSettings = atom<Settings>(getSettings());
|
||||
// Gitea #42 — settingsAtom used to be a one-shot snapshot with no cross-tab
|
||||
// sync at all (unlike atomWithLocalStorage.ts, which this mirrors). Without
|
||||
// this, a tab left open never sees settings changed in another tab until it
|
||||
// reloads, and (before the merge-on-write above) its next save would revert
|
||||
// them.
|
||||
baseSettings.onMount = (setAtom) => {
|
||||
const handleStorageChange = (evt: StorageEvent) => {
|
||||
if (evt.key !== STORAGE_KEY) return;
|
||||
setAtom(getSettings());
|
||||
};
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
};
|
||||
|
||||
export const settingsAtom = atom<Settings, [Settings], undefined>(
|
||||
(get) => get(baseSettings),
|
||||
(get, set, update) => {
|
||||
const previous = get(baseSettings);
|
||||
set(baseSettings, update);
|
||||
setSettings(update);
|
||||
setSettings(previous, update);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user