feat: load + debounced persist via tauri-plugin-store
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
92
src/lib/persist.ts
Normal file
92
src/lib/persist.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user