Skip to content

fix(frontend): cache the session server snapshot (2 more 'known noise' entries were real) - #64

Merged
Shaan-alpha merged 2 commits into
mainfrom
fix/session-snapshot-caching
Jul 31, 2026
Merged

fix(frontend): cache the session server snapshot (2 more 'known noise' entries were real)#64
Shaan-alpha merged 2 commits into
mainfrom
fix/session-snapshot-caching

Conversation

@Shaan-alpha

Copy link
Copy Markdown
Owner

Came out of auditing the known-noise console list in .claude/skills/verify — the list that had been hiding #62's a11y defect. Two more of its four entries turned out to be one real bug.

Root cause — one line, two symptoms

getServerSnapshot returned Promise.resolve(null) from its body, minting a fresh promise on every call:

function getServerSnapshot(): Promise<Session> {
  return Promise.resolve(null);   // new identity every call
}

useSyncExternalStore compares snapshots with Object.is, and use() needs a promise identity it can resolve against a cache. That single site produced both of these:

The result of getServerSnapshot should be cached to avoid an infinite loop
A component was suspended by an uncached promise

React's own wording — "to avoid an infinite loop" — says this was never cosmetic. It had been on the ignore list since the skill was written.

There is no server session to report (the cookie is read by the browser), so a single module-level resolved-null promise is the correct snapshot. getSnapshot was already correctly cached via the module-level cachedPromise; only the server path was wrong. use() appears exactly once in the codebase, which is why one fix clears both messages.

Verified in a browser, both directions

Landing page plus a client-side transition to a report, console events grouped by message:

main this PR
uncached promise 2 0
getServerSnapshot should be cached 1 0
Base UI nativeButton 1 1 (fixed by #62)
total 4 1

Rewrites the list that hid this

.claude/skills/verify told every future agent to treat four console errors as background noise and only "check the count didn't grow". Three of the four were real bugs. The list now:

  • names each fixed entry and what it actually was,
  • keeps only the anonymous 401 on /me as genuinely inert, with the reason (an anonymous visitor has no session; fetchSessionFresh handles it and returns null),
  • sets the baseline to 1 expected entry rather than 4, so a reappearance reads as a regression,
  • requires a written justification before anything is added back.

It also gains the two process rules this session paid for the hard way:

  • Verify portal/layout/paint claims in the browser, not vitest. happy-dom computes no layout and handles portals differently from Chrome. An <Activity> popover "leak" was chased and a fix written on jsdom evidence before Chrome showed the bug did not exist (see fix(frontend): badge trigger is not exposed as a button (audit finding 3) #62).
  • Drive route changes through the app's own UI, not Page.navigate. A full document load tears down client state and the App Router's <Activity> boundaries, so any test about back/forward or preserved state passes silently while proving nothing.

No unit test, deliberately

The invariant is a promise's referential stability across React's SSR/hydration path. getServerSnapshot is module-private, and reproducing the warning in vitest would mean a full server-render-plus-hydrate harness for a one-line constant — and happy-dom is exactly the environment I have just been burned trusting for this class of question. The guard is the CDP console baseline above, which the verify skill now states as a hard number rather than a vague "didn't grow".

Verification

vitest run 73 passed / 23 files, tsc --noEmit clean, eslint src clean, plus the CDP runs above against both branches.

Merge order

CI stays red on Backend until #60 merges. No file overlap with #59, #60, #61, #62 or #63. CHANGELOG/PROGRESS_LOG entries for the batch are consolidated in #59.

getServerSnapshot returned Promise.resolve(null) from its body, minting a
fresh promise on every call. useSyncExternalStore compares snapshots with
Object.is and use() needs a promise identity it can resolve against a
cache, so this single site produced both console errors the project had
learned to ignore:

  The result of getServerSnapshot should be cached to avoid an infinite loop
  A component was suspended by an uncached promise

React's own wording -- "to avoid an infinite loop" -- says this was never
cosmetic. There is no server session to report (the cookie is read by the
browser), so one resolved-null promise is the correct snapshot.

Verified in real Chrome over CDP, landing page plus a client-side
transition to a report: main emits 4 console errors, this branch emits 1
(the Base UI nativeButton warning, which #62 fixes).

Also rewrites the known-noise list in .claude/skills/verify that hid both
of these. Three of its four entries were real bugs; only the anonymous 401
on /me is genuinely inert. Baseline is now 1 expected entry, not 4. Adds
the two process rules this session paid for: verify portal/layout/paint
claims in the browser rather than vitest, and drive route changes through
the app's own UI rather than Page.navigate.
Copilot AI review requested due to automatic review settings July 31, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Shaan-alpha added a commit that referenced this pull request Jul 31, 2026
Three of the four entries on the verify skill's known-noise console list
were real bugs. Also removes a duplicated changelog bullet introduced when
the withdrawn finding-2 line was edited out.
@Shaan-alpha
Shaan-alpha merged commit ee885a8 into main Jul 31, 2026
5 checks passed
@Shaan-alpha
Shaan-alpha deleted the fix/session-snapshot-caching branch July 31, 2026 03:40
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.

2 participants