perf(roms): let a gallery window fetch skip the library count - #4062
Merged
Conversation
Opting out of the rom id index made the endpoint fall back to a COUNT over the whole filtered set, so every scroll batch re-derived a total the page already had from its bootstrap. Add a with_total flag that skips that count, and have the v2 gallery's window fetch pass it alongside the sidecar opt-outs it already sends. total comes back null there, which the store's existing guard leaves alone rather than applying over the size it knows. Fixes rommapp#4053 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Greptile SummaryThe PR adds an optional ROM-count opt-out and uses it for gallery window requests after initial metadata has established the result size.
Confidence Score: 5/5The PR appears safe to merge with no actionable correctness or security issues identified. The count opt-out remains default-on for existing callers, the gallery enables it only after establishing its total, nullable responses are guarded from overwriting that state, and tests cover both backend query execution and frontend lifecycle behavior. Important Files Changed
Reviews (1): Last reviewed commit: "perf(roms): let a gallery window fetch s..." | Re-trigger Greptile |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gantoine
approved these changes
Aug 2, 2026
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.
Description
Fixes #4053
Loading more covers into a gallery you are already looking at makes the server re-count
the whole filtered library, every single time.
The page already knows that total. It learned it on its first request, and nothing can
change it mid-scroll. But the flags a window fetch sends to save work
(
with_char_index=false,with_filter_values=false,with_rom_id_index=false) areexactly what puts the backend on its non-index path, and that path answers
totalwith aseparate
COUNTover the filtered set. So the opt-out meant to avoid a full-library scanquietly swapped it for a different one, and the frontend then wrote the result over the
identical number it already had.
This PR mirrors the existing
with_rom_id_indexopt-out with awith_totalone, and hasthe gallery's window fetch use it. Follow-up to #4054, where I flagged this as
deliberately out of scope.
What changed
backend/endpoints/roms/__init__.pywith_totalquery parameter (defaults totrue, so existing callers are unaffected) and skipsget_rom_countwhen it is false.CustomLimitOffsetPage.totalis redeclared asGreaterEqualZero | Noneso the page can carry a null.backend/tests/endpoints/roms/test_rom.pyget_rom_countand asserts it is never called, so it guards the query being skipped rather than merely the field being null, with a control request proving the count still runs by default. The second pins the deliberate asymmetry below.frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.tstotal: numberbecomestotal: (number | null).frontend/src/services/api/rom.tswithTotalonGetRomsParams, forwarded only when explicitly set. Purely additive, so no existing call site changes.frontend/src/v2/stores/galleryRoms.tsfetchWindowAtsendswithTotal: falsealongside the three sidecar opt-outs it already sends, but only once the bootstrap has established the size. The existing null guard ondata.totalthen leaves that size alone.frontend/src/v2/stores/galleryRoms.test.tsResults
Cost of the redundant
COUNTalone, per scroll batch, measured on my library and reportedin #4053:
The saving is the whole figure rather than a fraction of it, because this removes the
query instead of making it faster.
What a reviewer should look at
totalbecomes nullable in the API contract. This is the part worth scrutiny.Runtime behaviour is unchanged for every existing caller, since the flag defaults to
true, but the OpenAPI type widens fromnumbertonumber | null, which anygenerated client outside this repo will see. I declared it as
GreaterEqualZero | Nonerather than giving it a default, so the field stays required in the schema (nullable,
not optional) and keeps its
ge=0constraint.with_rom_id_index=truethe total is just
len(rom_id_index), already in hand, so suppressing it would savenothing and cost the caller information. That asymmetry is documented in the parameter
description and pinned by a test so it does not read as an oversight later.
check-v1-frozenstays green. The two v1 consumers oftotal(
RandomBtn.vue,ContextualRandomBtn.vue) already guard withif (!romsResponse.total), so they narrow the nullable type with no edit, andwithTotalis optional onGetRomsParams.SidecarOptions, the typefetchInitialMetadataaccepts, has nowithTotalmember, so the request thatestablishes the gallery's size structurally cannot skip the count.
resetGallery()andinvalidateWindows()cleartotalandmetadataLoaded, so the first request under anew filter always re-bootstraps with the count.
Testing
tests/endpoints/roms/test_rom.pyand the v2galleryRomsstore suite re-run individually.vue-tscclean,trunk fmtandtrunk checkclean.count-skip test asserts on the query not being issued, not just on the response field,
so it cannot pass by serialisation alone.
total,the follow-up window fetch sends
with_total=falseand comes back withtotal: null,and the gallery, its count badge and the AlphaStrip all render unchanged.
the second-and-later window path is covered by the store unit tests rather than by the
browser check. The timings above come from my large library, not from the dev instance.
Known follow-up, deliberately not in this PR
getRecentRomsandgetRecentPlayedRomsstill sendwith_rom_id_index=falseand discardthe
totalthey get back, so every home page load pays two counts for nothing. It is thesame one-line opt-out, but it lives in both
services/api/rom.tsandservices/cache/api.ts, and in the latter the parameter map is duplicated intoclearRecentRomsCache/clearRecentPlayedRomsCacheas a cache-key pattern. Adding theflag to the request without adding it there too would silently break cache invalidation,
so it deserves its own PR rather than a rider on this one.
AI assistance disclosure
This PR was written primarily by Claude Code (Claude Opus 5), including the backend
change, the frontend change, the tests and this description. I directed the work, reviewed
every change, and the performance numbers come from measurements against my own library
rather than from estimates.
Checklist