Fix Library A-Z selector with server-side letter indexing - #1245
Draft
skalthoff wants to merge 1 commit into
Draft
Fix Library A-Z selector with server-side letter indexing#1245skalthoff wants to merge 1 commit into
skalthoff wants to merge 1 commit into
Conversation
Member
|
Hold on with this one, #1236 is probably going to conflict this one to high hell when it supersedes Unless this builds off of that one 🙂 |
Member
|
@skalthoff conflicts are hot and ready for you :spaghetti |
skalthoff
force-pushed
the
fix/library-az-letter-jump
branch
from
June 12, 2026 03:32
160551f to
509bdce
Compare
The A-Z scroller paged through the library sequentially until the selected letter appeared in the loaded sections. The pagination loop also read hasNextPage and the section data from a stale render snapshot, so it always crawled to the end of the library and then spun forever on the frozen flag, and its found-letter check compared lowercase letters against uppercase section titles. On large libraries a single letter tap meant hundreds of sequential page fetches followed by a frozen JS thread. Letter taps now resolve the exact start index of the letter's section server-side and reposition the infinite query cache directly onto the page containing it: - SortName-ordered lists (artists; albums sorted by sort name) use a single NameLessThan count query. Ascending, the count of items before the letter is its start index; descending, the boundary is the complement of the count before the next letter. Two requests per jump in either direction. - Everything else (tracks sort by display name; Audio sort names are number-prefixed) uses a probe search over startIndex/limit=1 requests: interpolated between known letter positions, bracketed by prior samples, and narrowed only to page granularity - the exact boundary is resolved by scanning the page that gets fetched anyway. A cold jump on a 200,000 track library costs ~10 one-item probes; repeat jumps reuse the accumulated samples and approach zero. - Probe results are memoized per query key for the session, and the loaded window's edges seed the search bracket for free. - The downloaded-tracks filter jumps locally with no network. The scroll happens once the repositioned sections render, mapped by item offset to a section list location, so letters with no items land on the next section and descending sorts work. Also in this change: - Tracks gets the same five-page window as the other tabs (bounded memory on huge libraries) plus backward pagination - Section list pagination handlers are guarded against duplicate fetches, and the refresh spinner no longer flashes during ordinary infinite-scroll pagination - Tracks rows no longer scan the whole list twice per render to build the player queue slice; a single pass builds a flat index map - Section building appends in place instead of re-copying the section array per item - A-Z selector ignores gestures until layout settles (no divide by zero), always clears its spinner when a jump fails, and only fires haptics when the selected letter changes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A letter tap on the Library A-Z scroller pages through the library sequentially until the selected letter shows up in the loaded sections (
onLetterPaginateQuery). Three compounding issues make this unusable at scale:query.hasNextPageandquery.datanever update inside the loop, so the "found the letter" check can never observe fresh pages — and once the server runs out of pages,fetchNextPage()becomes an instantly-resolving no-op while the frozenhasNextPagestaystrue: a tight infinite loop that pegs the JS thread.localeCompare === 0.The scroll that follows also runs against the same stale snapshot, and
useTrackshas no page window — the crawl accumulates the whole library in memory.Fix
A letter tap now resolves the exact start index of the letter's section server-side and repositions the infinite query cache directly onto the page containing it. Request budget by list type:
The count fast path (
NameLessThan+Limit=1&EnableTotalRecordCount=true): the server appliesNameLessThanto the stored lowercasedSortNamein both the 10.10 SQLite and 10.11+ EF Core item repositories, so for SortName-ordered lists the count is the boundary index. Descending uses the complement:total − count(< nextLetter). One COUNT query either direction.The probe search (tracks — Audio
SortNames are disc/track-number prefixed, so name filters don't apply): a lower-bound search overstartIndex=N&limit=1probes with three scale optimizations:Probes carry ~1KB (
limit=1, no images, no user data,enableTotalRecordCountonly on the one-time total probe so the server doesn't run a COUNT per probe). Server-side, each probe is an index walk —BaseItemshas a composite index ending inSortNameon both server generations.At 200k tracks and ~150ms RTT that's roughly: count path ≈ 0.3–0.5s; cold probe search ≈ 1.5s; warm ≈ 0–1s — versus minutes followed by a frozen app today.
The scroll happens after the repositioned sections render (offset → section/item location), so letters with no items land on the next section and descending sorts work. Downloaded-tracks filter jumps locally with no network.
Also fixed
useTracksgets the same 5-page window as the other tabs (bounded memory at 200k) plus backward pagination so the window can scroll both ways after a jumpindexOfscans per rendered row to build the player queue slice (single-pass flat index map)flattenInfiniteQueryPagesappends in place instead of re-copying the section array per itemTests
jest/functional/LetterJump.test.ts(32 tests) covers the search math and the request budgets explicitly: cold/warm/same-letter probe counts on synthetic 200k libraries (uniform and 95%-skewed), descending order, missing letters,#edge cases, page-boundary location, section offset mapping, count-path arithmetic (asc + desc), window reuse (no fetch when the page is loaded), and the local downloaded-tracks path.Full suite: 14 suites / 134 tests passing,
tscand ESLint clean.Testing this PR
JS-only change — OTA bundles publish automatically for this PR. Worth exercising on a device, ideally against a big library: