Skip to content

perf(timeline): the reader-id fix shipped in a function the page never calls - #826

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/warm-reader-at-the-door
Aug 28, 2026
Merged

perf(timeline): the reader-id fix shipped in a function the page never calls#826
github-actions[bot] merged 2 commits into
mainfrom
fix/warm-reader-at-the-door

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Follow-up to #824. Two things that PR did not actually deliver.

What happened

#824 was squash-merged at the head the auto-merge sweep saw, which was one push behind — the getCurrentUserId consolidation never reached main. Verified against origin/main: the level-order getReplies landed, the consolidation did not.

And the part that was written had a bug of its own: I warmed the reader's id inside getUserFeed. The timeline page calls getEnrichedUserFeed. So even had it merged, it would have done nothing. Confirmed live after that deploy:

3147 → 3520  get_user_timeline_feed
3552 → 3784  /auth/v1/user            ← still after the feed
3811 → 4076  reactions ×3             ← still behind that

This PR

1. The consolidation (unchanged from #824, re-applied). getCurrentUserId was defined six times — timeline queries, timeline processors, groups, loans, projects, auth — each an uncached call to /auth/v1/user. Now one definition in the auth layer, caching the in-flight promise so concurrent callers collapse onto one request. Safe as module state only because that module talks exclusively to the browser client; the one caller that passes its own client (groups) keeps its uncached path. Cache is dropped on any auth state change, registered lazily on first use rather than at import. check:one-current-user fails the build on a seventh copy.

2. The warm, moved to a door that all reads pass through. The timeline service facade — so it covers getEnrichedUserFeed, getEnrichedFollowingFeed, getCommunityFeed, and also getEventById / getReplies / searchPosts / getThreadPosts, which have the same ordering problem on thread pages. One rule in one place, and no longer dependent on me guessing which function the page calls.

Tests assert the ORDER

A test that only checked "the warm happened" would pass with the call left at the end — which is exactly the bug. So the test records call order across every read the facade exposes, and fails if a new read is added without warming.

Mutation-proved — each red, then green after restore:

mutation result
getEnrichedUserFeed stops warming 1 red
warm moved to after the query (the bug I shipped) 11 red
a seventh getCurrentUserId definition gate red
the owner loses its definition gate red

The gate was also confirmed reachable through npm run, not only by hand.

Already verified live from #824

The level-order traversal works: opening a 5-reply thread now makes 1 /auth/v1/user call (was 8) and exactly 3 timeline_events queries — one per depth, not one per reply.

catomean and others added 2 commits August 29, 2026 00:00
getCurrentUserId was defined six times across services - timeline queries,
timeline processors, groups, loans, projects, and the auth layer. Every copy
called supabase.auth.getUser(), which is a NETWORK call: it validates the
token against /auth/v1/user. So a single page asked the server who the
reader was over and over, and the cache I added yesterday covered only four
of the six importers.

On the timeline that lookup also sat on the critical path. Measured on a
cold load:

  2429 -> 3061  get_user_timeline_feed   632ms
  3105 -> 3351  auth/v1/user             246ms   <- depends on nothing
  3367 -> 3595  reactions x3             228ms   <- waits on the one above

The identity lookup ran after the feed returned, and then the reaction
queries waited on it in turn, though it depends on neither.

Changes:

- One definition, in the auth layer, caching the in-flight promise so
  concurrent callers collapse onto one request. Safe as module state only
  because this module talks exclusively to the browser client; a per-request
  server client would hand one request's user to the next, and the one
  caller that passes its own client (groups) keeps its uncached path.
- The cache is dropped on any auth state change, registered lazily on first
  use rather than at import - this module is imported very widely and an
  import-time subscription is a side effect every importer pays for.
- getUserFeed warms the id alongside the feed request instead of after it,
  and asks for the total count concurrently rather than after enrichment.
- check:one-current-user fails the build if a seventh copy appears. Wired
  into verify.

A test caught a real behaviour change while writing this: getUser() catches
its own errors and reports them in `error` rather than throwing, so "could
not ask" arrived looking like "nobody is signed in" and would have been
cached as signed-out for the rest of the page. Fixed the code, not the test.

Mutation-proved: a seventh definition -> red; the owner losing its
definition -> red; both green after restore; and the gate is reached through
npm, not just when run by hand.

Full suite: 261 suites, 2524 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
…hrough

My previous commit warmed the reader's id inside getUserFeed. The timeline
page calls getEnrichedUserFeed. So the fix shipped to production and did
nothing: /auth/v1/user still started at 3552ms, after the feed returned at
3520, with the three reaction queries queued behind it.

Moved to the timeline service facade, which every read goes through -
including getEventById, getReplies, searchPosts and getThreadPosts, which
have the same ordering problem on thread pages. One rule in one place rather
than a call repeated inside each query function, and no longer dependent on
me picking the right one.

The test asserts the ORDER, not that the warm happens: a test that only
checked "it was called" would pass with the call left at the end, which is
the bug. It runs over every read the facade exposes and fails if a new one
is added without warming.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
@github-actions
github-actions Bot merged commit eb68d72 into main Aug 28, 2026
6 checks passed
@github-actions
github-actions Bot deleted the fix/warm-reader-at-the-door branch August 28, 2026 22:12
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
…ed page (#827)

warmCurrentUserId is called at the top of every timeline read, and timeline
reads happen during server rendering too. It started an unconditional
/auth/v1/user request from the SERVER using the BROWSER Supabase client,
which carries no request cookies - so it could only ever fail, and it failed
while the route was rendering.

That is not a wasted request, it is an outage. /timeline, /community and
/dashboard sat on their route-level loading.tsx skeleton forever on a fresh
page load. Navigating to the very same route from inside the app worked,
because that path never server-renders - which is what made it look
intermittent. The skeleton is near-white on a white page, so the symptom
users see is "the timeline is blank". Nothing in the console, no failed
request, every check green.

I introduced this in #826 and did not catch it, because I verified the fix
on the post page and by measuring the network waterfall, neither of which
exercises a cold server render of a feed route.

The warm is now a no-op on the server, silently: it is an optimisation, and
an optimisation must never be the reason a page fails.

The test runs in the `node` jest environment on purpose. Under jsdom
`window` exists, the guard is never exercised, and the test would pass while
the server path stayed broken - which is exactly how this shipped.
Mutation-proved: removing the guard turns it red.


Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5

Co-authored-by: Georgy Butaev <41178744+g-but@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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