Skip to content

feat: page the feed instead of downloading the whole department - #21

Merged
duuyguyiilmaz merged 1 commit into
mainfrom
feat/feed-pagination
Aug 31, 2026
Merged

duuyguyiilmaz merged 1 commit into
mainfrom
feat/feed-pagination

Conversation

@duuyguyiilmaz

Copy link
Copy Markdown
Owner

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 limit without an orderBy returns an arbitrary subset — so the sort moves to Firestore, and the query gains a composite index on department + createdAt.

Without that index 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:

firebase deploy --only firestore:indexes

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 limit as 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

  • 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.

Verification

Seven new tests, 107 → 114. Mutation-tested: 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.

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

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>
@duuyguyiilmaz
duuyguyiilmaz merged commit bc0f648 into main Aug 31, 2026
2 checks passed
@duuyguyiilmaz
duuyguyiilmaz deleted the feat/feed-pagination branch August 31, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant