Repository navigation
feat: page the feed instead of downloading the whole department - #21
Merged
Merged
Conversation
The feed read every note in the department on every open and sorted them in memory. Fine with three test notes, wrong at any real size: Firestore bills per document, so opening the feed cost one read per note in the department, and the sort ran on the main thread in the listener callback. Paging needs the order to come from the query — a `limit` without an `orderBy` returns an arbitrary subset — so the sort moves to Firestore, and the query gains a composite index on `department` + `createdAt`. That index is a hard dependency: without it the query fails with FAILED_PRECONDITION and the feed does not open at all, so firestore.indexes.json is committed and deploying it is now a numbered step in the README rather than something to discover from a stack trace. Paging works by growing one listener rather than stitching queries together. The feed is live, so a second query for page two would leave two windows with different freshness and no clear owner of the order; instead the same listener is re-subscribed with a wider limit as the user nears the end of the list. The cost is real and worth naming: widening re-reads what was already fetched, so 20 + 40 is 60 document reads rather than 40. That is still far cheaper than the whole collection on every open, and most readers never leave the first page. Nothing is hidden by this. Every note in the department is still reachable; it arrives 20 at a time instead of all at once. Three decisions the tests pin down: - `canLoadMore` is "the page came back full", because knowing the real total would cost another read. Being wrong costs one extra request on the last page, and it corrects itself: the same results come back and the flag goes false. - `loadMore()` is a no-op on the last page. The scroll listener fires on every scroll near the end, so without that guard it would rebuild the same window continuously. - Later pages do not pass through Loading. Doing so would clear the list from the screen mid-scroll. Seven new tests, 107 -> 114. Verified by mutation: removing the last-page guard and pinning canLoadMore to true turns three of them red. Not verified here: the index itself and the Firestore query behind it. The fakes stop at the repository interface, and this machine has no credentials for a live project — the same gap the README already records for every Firebase repository. Co-Authored-By: Claude Opus 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.
The feed read every note in the department on every open and sorted them in memory. Fine with three test notes, wrong at any real size: Firestore bills per document, and the sort ran on the main thread inside the listener callback.
Why the index is a hard dependency
Paging needs the order to come from the query — a
limitwithout anorderByreturns an arbitrary subset — so the sort moves to Firestore, and the query gains a composite index ondepartment+createdAt.Without that index the query fails with
FAILED_PRECONDITIONand the feed does not open at all. Sofirestore.indexes.jsonis committed, and deploying it is now a numbered step in the README rather than something to discover from a stack trace:Growing one listener, not stitching queries
The feed is live, so a second query for page two would leave two windows with different freshness and no clear owner of the order. Instead the same listener is re-subscribed with a wider
limitas the user nears the end.The cost is real and worth naming: widening re-reads what was already fetched, so 20 + 40 is 60 document reads rather than 40. Still far cheaper than the whole collection on every open, and most readers never leave the first page.
Nothing is hidden by this. Every note in the department is still reachable; it arrives 20 at a time instead of all at once.
Three decisions the tests pin down
canLoadMoreis "the page came back full", because knowing the real total would cost another read. Being wrong costs one extra request on the last page, and it corrects itself — the same results come back and the flag goes false.loadMore()is a no-op on the last page. The scroll listener fires on every scroll near the end, so without that guard it would rebuild the same window continuously.Loading. Doing so would clear the list from the screen mid-scroll.Verification
Seven new tests, 107 → 114. Mutation-tested: removing the last-page guard and pinning
canLoadMoreto true turns three of them red.Not verified here: the index itself and the Firestore query behind it. The fakes stop at the repository interface, and this machine has no credentials for a live project — the same gap the README already records for every Firebase repository.
Follow-up
The leaderboard still reads its whole department. It can take the same shape, but needs its own index on
department+ratingSum, and a ranking that shows only part of itself needs some thought about what "rank 21" means when the rest is not loaded. Left as a documented limitation.🤖 Generated with Claude Code