diff --git a/src/lib/persist.ts b/src/lib/persist.ts new file mode 100644 index 0000000..675a068 --- /dev/null +++ b/src/lib/persist.ts @@ -0,0 +1,92 @@ +import { load, type Store } from "@tauri-apps/plugin-store"; +import { STORE_FILE, STORE_KEY, type OverlayPersistedState } from "../types/overlay"; +import { useOverlayStore } from "./store"; +import { debounce } from "./debounce"; + +const DEFAULTS: OverlayPersistedState = { + url: "", + opacity: 1.0, + alwaysOnTop: true, + clickThrough: false, +}; + +let storePromise: Promise | null = null; +function getStore(): Promise { + if (!storePromise) { + storePromise = load(STORE_FILE, { defaults: {}, autoSave: false }); + } + return storePromise; +} + +function isPersistedShape(v: unknown): v is OverlayPersistedState { + if (!v || typeof v !== "object") return false; + const o = v as Record; + return ( + typeof o.url === "string" && + typeof o.opacity === "number" && + typeof o.alwaysOnTop === "boolean" && + typeof o.clickThrough === "boolean" + ); +} + +/** Load persisted state from disk and hydrate the Zustand store. Safe on first run. */ +export async function hydrateFromDisk(): Promise { + try { + const s = await getStore(); + const raw = await s.get(STORE_KEY); + const value = isPersistedShape(raw) ? raw : DEFAULTS; + useOverlayStore.getState().hydrate(value); + } catch (err) { + console.warn("Failed to load persisted state; using defaults", err); + useOverlayStore.getState().hydrate(DEFAULTS); + } +} + +// We persist on a 300ms debounce. The mirror below tracks "latest known +// pending state" so a synchronous flush at beforeunload time has something +// to issue — even though the actual write is unavoidably async. +let latestPending: OverlayPersistedState | null = null; + +async function writeNow(state: OverlayPersistedState): Promise { + try { + const s = await getStore(); + await s.set(STORE_KEY, state); + await s.save(); + } catch (err) { + console.warn("Failed to persist state", err); + } +} + +const writeDebounced = debounce((state: OverlayPersistedState): void => { + latestPending = null; + void writeNow(state); +}, 300); + +/** Subscribe to store changes and persist the four user-pref fields. */ +export function startPersistSubscription(): () => void { + return useOverlayStore.subscribe((s) => { + const snapshot: OverlayPersistedState = { + url: s.url, + opacity: s.opacity, + alwaysOnTop: s.alwaysOnTop, + clickThrough: s.clickThrough, + }; + latestPending = snapshot; + writeDebounced(snapshot); + }); +} + +/** + * Best-effort synchronous flush for beforeunload. The actual disk write is + * unavoidably async — we fire-and-forget it. If the window is destroyed + * before the write lands, the prior debounce-tick's value (at most 300ms + * stale) will be loaded next session. That bounded staleness is the + * accepted trade-off for a Phase 1 utility app. + */ +export function flushPendingPersistSync(): void { + if (latestPending === null) return; + const snapshot = latestPending; + latestPending = null; + writeDebounced.cancel(); + void writeNow(snapshot); +}