Repository navigation
Paginate folder listings: keep huge folders fast end to end - #24
Merged
Merged
Conversation
Large folders previously arrived as one unbounded JSON response (23 MB at
100k files) and mounted every tile at once. Three changes make folder
browsing O(page) end to end with no visible UX change:
- Keyset pagination on GET /api/folders/{id} and the shared-folder
endpoints: files page on (lower(name), name) via files_folder_name_idx,
500 per page (file_limit caps at 1000). The keyset condition is spelled
as a range term plus tie filter because SQLite will not seek the index
for a row-value comparison with bound parameters. Cursor pages carry
only the files window; the folder envelope ships once on page one.
- gzip for application/json responses (listing JSON compresses ~20x, a
500-file page is ~5.5 KB on the wire); downloads pass through untouched.
- The web app streams remaining pages in the background after painting
page one, keeping the full listing in memory so select-all, sorting,
and preview navigation still see every file, while only a window of
tiles/rows is mounted — an IntersectionObserver sentinel extends it
two viewports ahead of scroll.
Benchmarks (BenchmarkScaleList*): first and deepest page of a 100k-file
folder both serve in ~1.1 ms / ~115 KB raw; the 200-file-folder listing
stays flat as unrelated files grow 1k -> 100k.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Why
The README promises Holdfast "stays fast even with lots of files." Benchmarks showed the folder listing endpoint returned every file in one unbounded JSON response — 23 MB and ~95 ms server-side at 100k files — and the web app then mounted every tile at once. This PR makes folder browsing O(page) at every layer, with no visible change to the design or UX.
What
Backend
GET /api/folders/{id}and the shared-folder endpoints. Files page on(lower(name), name)— exactly the orderfiles_folder_name_idxstores — 500 per page (file_limitcaps at 1000). Small folders respond byte-for-byte as before: one response, no cursor.lower(name) >= ? AND (lower(name) > ? OR name > ?)) because SQLite will not seek the index for a row-value comparison with bound parameters — it silently scans the folder from the top. Caught by benchmark, not by tests: the last page of a 100k-file folder was 14 ms before, ~1 ms after.application/jsonresponses (new middleware, downloads/previews pass through). Listing JSON compresses ~20×: a 500-file page is ~5.5 KB on the wire.Frontend
loadSequence) drop pages still streaming for a folder you've navigated away from; in-place refresh assembles all pages off-screen and swaps once.Numbers
*before the covering-index fix in migration 012; benchmarks live in
internal/server/scale_bench_test.go.Verified
go test ./..., new pagination + gzip tests, race-free vetsvelte-check/ lint / build clean; all 16 Playwright full-stack e2e passKnown trade-off: the scrollbar lengthens as the render window extends (standard infinite-scroll behavior); true virtualization with reserved heights is the future upgrade if 50k-image folders on low-RAM phones become a target.
🤖 Generated with Claude Code