fix(memory): bound visualizer PCM cache and feed episode cache
The visualizer's PCM cache decoded the entire episode into RAM (22050 Hz mono s16 ~160 MB/hr of audio) and held it until stop() — a 3-hour episode pinned ~500 MB and long-form content hit 2.5 GB. The 4x decode also pulled the whole remote file even when only minutes were listened to. - audio-pcm-cache: sliding window around the playback position — the decode head caps at maxAheadSec (600s) ahead of the cursor, segments older than keepBehindSec (300s) are pruned, and the tail refills as playback advances. Steady state ~40 MB regardless of episode length; a backward seek past the window restarts a segment there (the existing seek-hole mechanism, no new failure mode). - feed: cap the full-parse episode cache at 1000 episodes/feed so archive-heavy subscriptions can't pin their entire history in RAM; the visible list stays bounded by the user's cache preference and fetch-more keeps working within the ceiling. - tests: pin the new head-cap and prune contracts (8/8 in audio-pcm-cache.test.ts; full suite 193 pass). Also includes the in-flight cleanup/refactor pass (cover-art resolve helper, page and comment tightening, ESLint config removal).
This commit is contained in:
@@ -77,30 +77,15 @@ function SearchPage() {
|
||||
const submittedQuery = (): string => stack()[1]?.ctx ?? searchStore.query();
|
||||
|
||||
// ── input focusing ────────────────────────────────────────────────────────
|
||||
// `inputFocused` is true while the query input is being typed in. The Shell
|
||||
// router yields keys to the <input> while this is true; Escape (in Shell)
|
||||
// sets it false so navigation resumes; `s` (search action) sets it true.
|
||||
//
|
||||
// The input's REAL focus is the source of truth for the flag:
|
||||
// useInputFocusNav (the same hook the Settings forms use) flips
|
||||
// `inputFocused` from the input's FOCUSED/BLURRED events, keeping the flag
|
||||
// and the renderable in lockstep. That matters when the user clicks OFF the
|
||||
// input: opentui's mouse dispatch auto-focuses the clicked target's nearest
|
||||
// focusable ancestor (a pane scrollbox), blurring the input. The BLURRED
|
||||
// event drops the flag, so the Shell router immediately resumes j/k/h
|
||||
// instead of swallowing keys with no input to receive them — no more
|
||||
// stuck "typing" state where Esc/j/k/s all do nothing.
|
||||
//
|
||||
// The depth stack still SEEDS the flag on transitions, since the query
|
||||
// depth defaults to typing: re-entering depth 0 (h back from results, or a
|
||||
// fresh mount) focuses the input; mounting at depth 1 (returning to the
|
||||
// tab after a search) stays list-navigation — a stuck-on flag there would
|
||||
// have the Shell yield j/k to a non-existent input. The depth STACK signal
|
||||
// is also written by focus moves (setDepthFocus), so gate the seed on the
|
||||
// depth VALUE via a memo: the effect must re-run only on an actual depth
|
||||
// transition. Without the memo every j/k at the query depth re-focuses the
|
||||
// input (undoing Escape), which keeps the recents list unreachable by
|
||||
// keyboard.
|
||||
// `inputFocused` tells the Shell router to yield keys to the query input.
|
||||
// The input's REAL focus is the source of truth: useInputFocusNav flips
|
||||
// the flag from the input's FOCUSED/BLURRED events, so clicking off the
|
||||
// input drops it and the router resumes j/k/h — no stuck "typing" state.
|
||||
// The depth stack only SEEDS it on transitions (re-entering depth 0
|
||||
// focuses the input; mounting at depth 1 stays list-nav), gated on the
|
||||
// depth VALUE via a memo because setDepthFocus also writes the stack
|
||||
// signal — without the memo every j/k at query depth re-focuses the input
|
||||
// and strands the recents list.
|
||||
onMount(() => nav.setInputFocused(depth() === 0));
|
||||
onCleanup(() => nav.setInputFocused(false));
|
||||
const focusNavRef = useInputFocusNav();
|
||||
|
||||
Reference in New Issue
Block a user