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:
57
src/stores/persist.ts
Normal file
57
src/stores/persist.ts
Normal 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 };
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../utils/app-persistence";
|
||||
import { useFeedStore } from "./feed";
|
||||
import type { SearchResult, SearchScope } from "../types/source";
|
||||
import { createPersistScheduler } from "./persist";
|
||||
|
||||
const STORAGE_SCOPE_KEY = "podtui_search_scope";
|
||||
const MAX_HISTORY = 10;
|
||||
@@ -70,6 +71,15 @@ export function createSearchStore() {
|
||||
const [selectedSources, setSelectedSources] = createSignal<string[]>([]);
|
||||
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
|
||||
* soon as the file is read). */
|
||||
async function init(): Promise<void> {
|
||||
@@ -167,24 +177,18 @@ export function createSearchStore() {
|
||||
};
|
||||
|
||||
const addToHistory = (q: string) => {
|
||||
setHistory((prev) => {
|
||||
const updated = sanitizeHistory([q, ...prev]);
|
||||
saveSearchHistoryToFile(updated);
|
||||
return updated;
|
||||
});
|
||||
setHistory((prev) => sanitizeHistory([q, ...prev]));
|
||||
persistHistory.schedule("search-history");
|
||||
};
|
||||
|
||||
const clearHistory = () => {
|
||||
setHistory([]);
|
||||
saveSearchHistoryToFile([]);
|
||||
persistHistory.schedule("search-history");
|
||||
};
|
||||
|
||||
const removeFromHistory = (q: string) => {
|
||||
setHistory((prev) => {
|
||||
const updated = prev.filter((h) => h !== q);
|
||||
saveSearchHistoryToFile(updated);
|
||||
return updated;
|
||||
});
|
||||
setHistory((prev) => prev.filter((h) => h !== q));
|
||||
persistHistory.schedule("search-history");
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
|
||||
Reference in New Issue
Block a user