feat: load + debounced persist via tauri-plugin-store

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-05-08 18:51:14 -04:00
parent a91be65354
commit fdeb6e8a19

92
src/lib/persist.ts Normal file
View File

@@ -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<Store> | null = null;
function getStore(): Promise<Store> {
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<string, unknown>;
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<void> {
try {
const s = await getStore();
const raw = await s.get<unknown>(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<void> {
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);
}