feat(B.1 #11): versioned settings schema with forward migration
Adds src/lib/utils/settingsMigrations.ts exposing
loadSettingsWithMigration() / saveSettingsWithVersion() around a
{version, data} envelope in localStorage["kon_settings"]. The
migration chain is indexed by destination version so adding a v2
is one MIGRATIONS[2] = (prev) => next entry away from working.
Legacy bare-object settings blobs are treated as v0 and folded into
v1 identically to before — no user-facing reset — but an unreadable
blob now surfaces a single Settings reset toast instead of silently
dropping data.
Covers Handy #602 ('settings reset on update') and unblocks the
other B.2/B.3 SettingsState additions listed in
docs/whisper-ecosystem/workstream-B.md: every subsequent field
lands behind a MIGRATIONS step, so older Kon builds stay readable.
Co-authored-by: jars <jakejars@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,10 @@ import type {
|
||||
import { toasts } from "$lib/stores/toasts.svelte.ts";
|
||||
import { errorMessage } from "$lib/utils/errors.js";
|
||||
import { parseStoredJson } from "$lib/utils/storage.js";
|
||||
import {
|
||||
loadSettingsWithMigration,
|
||||
saveSettingsWithVersion,
|
||||
} from "$lib/utils/settingsMigrations.js";
|
||||
import { hasTauriRuntime } from "$lib/utils/runtime.js";
|
||||
|
||||
export const page = $state<PageState>({
|
||||
@@ -68,9 +72,25 @@ function canUseStorage(): boolean {
|
||||
|
||||
function loadSettings(): SettingsState {
|
||||
if (!canUseStorage()) return { ...defaults };
|
||||
// Versioned migration: reads the envelope or an unversioned legacy
|
||||
// blob, runs the migration chain (currently identity: bare object ->
|
||||
// v1), and returns the current-shape data. Falls back to defaults
|
||||
// on corruption — the toast below surfaces the reset to the user.
|
||||
const migrated = loadSettingsWithMigration<Partial<SettingsState>>(SETTINGS_KEY);
|
||||
if (migrated === null && localStorage.getItem(SETTINGS_KEY) !== null) {
|
||||
// There was data but it failed to migrate — tell the user we
|
||||
// reset rather than silently mangling their preferences.
|
||||
queueMicrotask(() => {
|
||||
toasts.warn(
|
||||
"Settings reset",
|
||||
"Your saved settings couldn't be read, so Kon fell back to defaults. "
|
||||
+ "Earlier Kon builds can still read your old data.",
|
||||
);
|
||||
});
|
||||
}
|
||||
return {
|
||||
...defaults,
|
||||
...(parseStoredJson<Partial<SettingsState>>(localStorage.getItem(SETTINGS_KEY)) ?? {}),
|
||||
...(migrated ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,9 +98,11 @@ export const settings = $state<SettingsState>(loadSettings());
|
||||
|
||||
export function saveSettings() {
|
||||
if (!canUseStorage()) return;
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
} catch {}
|
||||
// Versioned envelope {version, data}. Older Kon builds that used
|
||||
// the bare-object layout can still spread the `data` key over
|
||||
// their defaults — they will just silently drop fields they don't
|
||||
// know about, which is the same behaviour as today.
|
||||
saveSettingsWithVersion(SETTINGS_KEY, { ...settings });
|
||||
}
|
||||
|
||||
function loadProfiles(): Profile[] {
|
||||
|
||||
Reference in New Issue
Block a user