refactor(persist): one per-domain persist scheduler

stores/persist.ts: createPersistScheduler — trailing-edge debounce,
flush, per-domain isolation. search-history writes collapse from
per-keystroke to one debounced write; scope save stays direct.
This commit is contained in:
2026-09-03 08:33:11 -04:00
parent 6aac138629
commit c2ec356a5f
2 changed files with 72 additions and 11 deletions

57
src/stores/persist.ts Normal file
View File

@@ -0,0 +1,57 @@
/**
* Persistence scheduler for PodTUI
* Per-domain trailing-edge debounced writes
*/
/** Debounced writer: rapid schedules collapse into one write per domain. */
export interface PersistScheduler {
/** Mark a domain dirty and (re)arm its trailing-edge write timer. */
schedule(domain: string): void;
/** Write the domain immediately if dirty; cancels any pending timer. */
flush(domain: string): void;
/** Write every dirty domain immediately. */
flushAll(): void;
}
/** Timer handle as returned by setTimeout in this runtime. */
type TimerHandle = ReturnType<typeof setTimeout>;
/** Build a per-domain trailing-edge debouncer. `write` is invoked with the
* domain name; callers read current state inside it, so a flush always
* lands the latest value. Rapid schedule() calls share one timer. */
export function createPersistScheduler(
write: (domain: string) => void,
debounceMs = 250,
): PersistScheduler {
const dirty = new Set<string>();
const timers = new Map<string, TimerHandle>();
const flush = (domain: string): void => {
const timer = timers.get(domain);
if (timer) {
clearTimeout(timer);
timers.delete(domain);
}
if (!dirty.has(domain)) return;
dirty.delete(domain);
write(domain);
};
const schedule = (domain: string): void => {
dirty.add(domain);
clearTimeout(timers.get(domain));
timers.set(
domain,
setTimeout(() => {
timers.delete(domain);
flush(domain);
}, debounceMs),
);
};
const flushAll = (): void => {
for (const domain of [...dirty]) flush(domain);
};
return { schedule, flush, flushAll };
}

View File

@@ -11,6 +11,7 @@ import {
} from "../utils/app-persistence"; } from "../utils/app-persistence";
import { useFeedStore } from "./feed"; import { useFeedStore } from "./feed";
import type { SearchResult, SearchScope } from "../types/source"; import type { SearchResult, SearchScope } from "../types/source";
import { createPersistScheduler } from "./persist";
const STORAGE_SCOPE_KEY = "podtui_search_scope"; const STORAGE_SCOPE_KEY = "podtui_search_scope";
const MAX_HISTORY = 10; const MAX_HISTORY = 10;
@@ -70,6 +71,15 @@ export function createSearchStore() {
const [selectedSources, setSelectedSources] = createSignal<string[]>([]); const [selectedSources, setSelectedSources] = createSignal<string[]>([]);
const [scope, setScopeState] = createSignal<SearchScope>(loadScope()); const [scope, setScopeState] = createSignal<SearchScope>(loadScope());
/** History persistence: rapid mutations collapse into one debounced
* write; the closure reads the live signal so a flush lands the
* latest list. */
const persistHistory = createPersistScheduler((domain: string) => {
if (domain === "search-history") {
saveSearchHistoryToFile(history());
}
});
/** Load search history from file (fire-and-forget; recents appear as /** Load search history from file (fire-and-forget; recents appear as
* soon as the file is read). */ * soon as the file is read). */
async function init(): Promise<void> { async function init(): Promise<void> {
@@ -167,24 +177,18 @@ export function createSearchStore() {
}; };
const addToHistory = (q: string) => { const addToHistory = (q: string) => {
setHistory((prev) => { setHistory((prev) => sanitizeHistory([q, ...prev]));
const updated = sanitizeHistory([q, ...prev]); persistHistory.schedule("search-history");
saveSearchHistoryToFile(updated);
return updated;
});
}; };
const clearHistory = () => { const clearHistory = () => {
setHistory([]); setHistory([]);
saveSearchHistoryToFile([]); persistHistory.schedule("search-history");
}; };
const removeFromHistory = (q: string) => { const removeFromHistory = (q: string) => {
setHistory((prev) => { setHistory((prev) => prev.filter((h) => h !== q));
const updated = prev.filter((h) => h !== q); persistHistory.schedule("search-history");
saveSearchHistoryToFile(updated);
return updated;
});
}; };
const clearResults = () => { const clearResults = () => {