The three `getLibraryBrowseItems` pickers (RerunCollectionsScreen, PlaylistsScreen, FillerPresetsScreen) populated a native <select> from a 100-row window over media-library tables that can hold tens of thousands of rows. #644 made that truncation visible; it did not make the picker usable, and paging to completeness would have been worse than the bug (~200 serial requests, each more expensive than the last). They now resolve by SEARCH through the shared `SearchPicker` over a new `searchLibraryPickerOptions` helper: zero requests on mount or on a type switch, at most ONE bounded request (25 rows) per settled query, nothing below 2 characters. Typed text is compiled via the now-shared `titleContainsQuery` (`title:*<escaped>*`) rather than forwarded raw, since the index's default field does not match bare title words. The current selection renders from the owning record — `selectedName` for rerun collections and playlist items, and for filler presets (which store only an id) a single by-id detail read — so editing an existing record can never lose or fail to name its selection. Class A stays put: bounded-by-construction admin lists still page to completeness via `loadAllPages`, and the collection-family filler-preset types keep their bounded single page (their `query` is a SQL LIKE, which a compiled Lucene query would not match). No server-side cap is raised; this is a web-only change. Folded in from #578: the rule-builder facet typeahead arms on focus rather than on mount (an N-rule tree fired N unrequested lookups), both typeaheads pair their `seqRef` guard with a shared `useIsMountedRef`, and the roundtrip test's LCG divides by 2^32 so `pick()` can no longer index one past the end. Decision record `spa.list-completeness-vs-bounded-pickers` is archived as superseded by the new `spa.library-pickers-resolve-by-search`; spa-conventions §3b rewritten to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.0 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| spa.list-completeness-vs-bounded-pickers | 2026-07-26 — `loadAllPages` is for bounded-by-construction lists only; media-library pickers stay bounded and show truncation (#644 follow-up) | superseded | 2026-07-26 | none | spa.library-pickers-resolve-by-search@2026-07-26 | (superseded) The shared `loadAllPages` helper (`web/src/api/paging.ts`) pages a `/api/v1` list to completeness against `totalCount` and is used ONLY for lists that are bounded by construction (rerun collections, multi-collections, playlists — admin-created, hundreds of rows at most). A `getLibraryBrowseItems` picker over a media-library table (Episode/Song/Image/Movie/MusicVideo, tens of thousands of rows possible) must NOT page to completeness — it fetches ONE bounded page (the server cap) and surfaces the truncation (a `ctv-field-help` hint wired to the real `totalCount`) instead of silently dropping the rest. | `loadAllPages`, Class A vs Class B picker, LuceneSearchIndex.Search hitsLimit, picker truncation hint, ctv-field-help, PagedResult, `complete` flag · paths: `web/src/api/paging.ts`, `web/src/screens/RerunCollectionsScreen.tsx`, `web/src/screens/PlaylistsScreen.tsx`, `web/src/screens/FillerPresetsScreen.tsx`, `web/src/screens/MultiCollectionsScreen.tsx`, `docs/spa-conventions.md` §3b · issues: #644 | superseded by `spa.library-pickers-resolve-by-search` (ersatztv#651) — Class A (`loadAllPages` for bounded-by-construction lists) survives there unchanged; only the Class B rule is reversed. See `docs/spa-conventions.md` §3b |
fe342a6a (#644) extracted the loadAllPages client-side paging helper and applied it at every
call site that had been requesting an over-cap pageSize to "get everything in one call" — a
pattern that silently truncated to the server's MaxPageSize (100) with no error and no
truncation indicator. A cold adversarial review of that fix found it was correct for the
admin-created lists (rerun collections, multi-collections, playlists — bounded by construction,
hundreds of rows at most) but dangerous for three call sites: the getLibraryBrowseItems pickers
in RerunCollectionsScreen, PlaylistsScreen, and FillerPresetsScreen, which populate a native
<select> whose mediaType can be Episode, Song, Image, Movie, or MusicVideo — the
largest tables in an install. Paging one of those to completeness means on the order of 200 serial
requests against a 20,000-row library, each more expensive than the last (LuceneSearchIndex .Search computes hitsLimit = skip + limit, so later pages re-scan a growing prefix), ending in a
<select> with 20,000 <option> nodes rendered into the DOM. That is worse than the defect #644
set out to fix.
The fix keeps loadAllPages unchanged in behavior for the bounded lists (it now also reports a
complete: boolean flag and accepts an AbortSignal, per the same follow-up review's F4/F2
findings) and removes it entirely from the three media-library picker call sites. Those instead
call getLibraryBrowseItems directly for a single page at the server cap (pageSize: 100) and
read the response's totalCount to detect truncation. The defect named in #644's title is
"silently truncate" — the silence is the bug, not the bound. So a truncated picker load renders a
ctv-field-help hint next to the <select> (Showing the first 100 of 5000 — use search to narrow.) instead of either paging forever or truncating without saying so. A full
typeahead/search-driven picker over the media library is a materially larger feature (a query
param already exists on getLibraryBrowseItems for it) and is deliberately out of scope here — a
follow-up issue, not this fix.
2026-07-26 addendum (round-3 review F1): loadPickerOptions's multi branch (a Class A
source — MultiCollection) reused the same truncated: boolean field as the Class B media-library
pickers, but the two conditions are not the same thing: Class B's truncated means "there are more
rows than fit in one page — narrow via search," while a Class A picker's flag meant "the
loadAllPages loop didn't converge" (complete: false) — a defensive/incomplete load, not a cap.
Rendering both through the shared "Showing the first N of M — use search to narrow" copy produced a
self-contradictory "Showing the first 47 of 47" on an incomplete Class A load, pointing at a search
box that picker doesn't have. RerunCollectionsScreen.tsx/PlaylistsScreen.tsx now return a
hint: 'incomplete' | 'none' | 'truncated' discriminator instead of a boolean, and render distinct
copy per value — 'truncated' keeps the existing search-narrowing text, 'incomplete' renders
"List may be incomplete — retry to reload" (matching the wording already used for the Class A
list-load warn Badge). A picker's console.warn on an incomplete load — and the analogous one in
SchedulesScreen.loadAllRerunCollections — is also gated on !signal?.aborted, so a superseded or
user-aborted load (Retry, or a type switch mid-load) no longer logs a false "did not complete"
warning.