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";
|
} 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 = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user