Skip to content

Page audit: backport React 19.3's hydration replay fix; a failed chunk reloads the page once - #605

Merged
NewWorldVenture merged 7 commits into
mainfrom
claude/roadmap-implementation-ld8bon
Sep 27, 2026
Merged

NewWorldVenture merged 7 commits into
mainfrom
claude/roadmap-implementation-ld8bon

Conversation

@NewWorldVenture

@NewWorldVenture NewWorldVenture commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Two fixes from the page audit (finalaudit.md, "Page Audit — every page on www.bubaly.com"). main (#601) is merged in.

React #418: backport React 19.3's fix for react/react#37584 (owner-approved)

The audit's last intermittent defect: React #418 on about 0.7% of signed-in loads, a different page each time, on a tree identical on the server and the client (8 rows 🔒 OWNER in the ledger).

The React that Next 15.5 bundles (19.2.0-canary-0bdb9206) has a bug. When a host element suspends mid-hydration, React replays it without rewinding the hydration cursor, and the replay tries to claim the element against its own first child. An instrumented build showed that pattern in 7 of 7 hits.

  • scripts/react-hydration-replay-fix.cjs is a webpack loader. It injects the fix React itself shipped, read from Next 16.3.6's bundled React 19.3.0-canary-cbb046ab. The fix goes at the same place React put it: the replay's HostComponent case, right after resetHooksOnUnwind. It applies to Next 15.5's react-dom-client, production and development, stable and experimental. The hydration helpers the fix calls are identical in 19.2 and 19.3.
  • A React whose site already carries the fix is left unchanged. Next 16's is, so Fix hydration replay with Next 16 #602 can land after this without a collision.
  • next.config.mjs adds a webpack() hook with one rule for those files. It touches no hunk that Fix hydration replay with Next 16 #602 changes.
  • tests/react-hydration-replay-fix.test.ts checks:
    • the fix applies to the installed React, directly after the hooks reset and before default:;
    • the patched React still parses;
    • the loader is idempotent;
    • Next 16's case, as a verbatim fixture, comes back unchanged;
    • an unknown React throws;
    • the build wiring is in place.
  • Proof, on a local production build of this source:
    • The framework chunk carries case 5:al(n);var r=n;r===rP&&(rL?(rM(r),5===r.tag&&null!=r.stateNode&&(rN=r.stateNode)):(rM(r),rL=!0));default:….
    • The Translate the public marketing site into every locale #418 hunt (273 signed-in routes × 3 rounds, 4 in parallel) found 0 of 819. The unpatched build had 7 of 819. An earlier form of the backport, a rewind at the top of the function, was also 0 of 819.

A chunk that fails to load reloads the page once; the root error page reads at AA

The production signed-out re-crawl (393 routes × phone and desktop, 786 loads) hit 8 loads where one JavaScript chunk answered 502. Each of those pages sat on "Something went wrong", whose Try again re-renders the same bundle and fails again. The chunks answer 200 on a re-fetch.

  • lib/utils/stale-bundle-reload.ts handles the reload:
  • It is wired into app/global-error.tsx, app/error.tsx, app/(app)/error.tsx and SectionError, which covers every section boundary. The Kitchen Display keeps its own policy.
  • The root error page's Try again button was white on #7c5dff, 4.31:1. It now uses the brand #744be8, 5.36:1. The reference line goes from 4.35:1 to 5.59:1.

Checks run

  • Full unit suite on Node 24: 20,869 passed. 3 whole-repo scans timed out at 5 s while the hunt loaded the CPU; all 3 pass alone (42/42).
  • eslint on the changed files: clean.
  • A targeted tsc on the changed files: clean.
  • next build: passes.
  • Signed-in crawl of all 393 routes, phone and desktop, on a local build of this source: 0 axe violations, 0 overflow, 0 page errors, 0 5xx.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K

The page audit's last intermittent defect: React #418 on about 0.7% of
signed-in loads, a different page each time, on a tree identical on the
server and the client. The React that Next 15.5 bundles
(19.2.0-canary-0bdb9206) replays a host element that suspended
mid-hydration without rewinding the hydration cursor, so the replay tries
to claim the element against its own first child. An instrumented build
showed that shape in 7 of 7 hits.

A webpack loader applies the upstream fix to Next's compiled
react-dom-client (production and development, stable and experimental):
before the replay, a host fiber that is the current hydration parent and
has claimed a node puts the cursor back on that node. It fails the build,
not silently, if Next's React changes shape, so an upgrade prompts its
removal. The owner approved the backport.

tests/react-hydration-replay-fix.test.ts holds that it applies to the
installed React, lands at the top of replaySuspendedUnitOfWork, is
idempotent, refuses a React it does not recognise, and is wired into the
build. Verified in a production build: the framework chunk carries the
rewind with its identifiers mapped to isHydrating, hydrationParentFiber,
nextHydratableInstance and popToNextHostParent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…or page reads at AA

The production re-crawl (786 loads) hit it 8 times: one JavaScript chunk
answered 502, and the page sat on "Something went wrong", whose Try again
is reset(), which re-renders the same bundle and asks for the same chunk.
The chunks answer 200 from Vercel on a re-fetch; the failure is transient
(a request lost in transit, or a deploy replacing the chunk under an open
tab), and only a reload recovers from it.

Every error boundary (the root, the app, and each section's SectionError)
now reloads the page once when the error is a chunk failure, and not again
for the same page within a minute, so a chunk that is really gone shows the
card instead of looping. The Kitchen Display keeps its own policy.

The same crawl's axe pass caught the root error page's Try again button,
white on #7c5dff at 4.31:1; it is the brand, #744be8, at 5.36:1, and the
reference line moves from 4.35:1 to 5.59:1.

tests/a-failed-chunk-reloads-the-page.test.ts holds the reload decision,
that every boundary makes it, and both contrasts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Sep 27, 2026 7:38pm UTC

Request Review

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

The same upstream hydration replay defect is already covered by draft #602. Its combined candidate f9adfd7 includes #601's current 52d2a52 session/meal repairs and current main e96e745. It uses Next 16.3.6's shipped renderer, retains React 18 packages, and introduces no vendor patch.

Independent results on that exact combined source: 20,856 UTC tests across 1,638 files; production build; full generated types; lint (15 existing warnings); production dependency audit (zero); and 278 controlled browser cases, including the actual renderer replay counterexample, session ownership and weekly meals. Route manifests lose zero application routes. Old #602 hosted CI passed with two retry-only flakes, so it is not being represented as clean acceptance of the combined source. Fresh combined hosted CI follows the final workflow/audit checkpoint.

Please coordinate the framework files rather than merging both approaches: your loader deliberately rejects unknown renderer anchors and could collide with the already-fixed Next 16 renderer. The chunk-recovery and contrast changes here are separate and I am reviewing them for the next integration batch. #601 is already in fresh hosted CI and remains the next release candidate.

We also found that package changes trigger the production migration workflow. The framework candidate is being guarded so all five mutation steps require an explicit manual boolean apply; automatic pushes retain read-only checks. No production SQL/retry or migration history change is authorized by this coordination.

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

Independent review of the chunk-recovery portion at 6758a6c (excluding the hydration loader and Next config, which overlap #602):

  • Native Chromium with real sessionStorage and window.location.reload() passed 10/11 bounded cases. Normal recovery, duplicate boundary effects, expiry, malformed markers, query-path separation, fully blocked storage, and write denial behave as intended.
  • One controlled failure injection exposes a reload loop: if getItem() throws while setItem() succeeds, readLast() converts the read failure to null, so every new document reloads again. The fixture stopped it after five native navigations. This is a simulated partial-storage failure, not an observed production incident or a claim about default browser storage policy.
  • A small temporary fix distinguishes unreadable storage (undefined, no reload) from an absent/invalid marker (null, one permitted recovery). The same 11 native cases then pass, with no app server, provider requests, or production writes.
  • Rendering the actual root error component confirms contrast improvements: white button text 4.315→5.365 and the reference line 4.355→5.588. Both updated values exceed 4.5:1.

I’m retaining the entrypoint regression and completing focused source/type/lint checks for the six chunk-recovery paths before the next integration batch. No authoritative source changes have been made by this review.

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

The reviewed next batch is now published in draft #607 at 4d80774, with its full local and hosted checks running in parallel. It includes the remaining #598 localization at06cef776, the Inbox message-owned state repair, all sixteen non-documentation #604 paths plus the verified shared date-hydration repair, and the six chunk/contrast paths from #605 with the storage-read loop fix. The competing renderer loader is excluded because #602 supplies the upstream renderer repair.

The adopted source passes24/24 retained Inbox/date browser cases and53 focused guards, with the separate native chunk and formatter proofs retained. finalaudit.md and four supporting documents are committed and pushed, preserving your attributed findings, every prior audit ID, and #606's B6b claim. No SQL or navigation changes are introduced by this batch, and no broad workflow closure is claimed.

#601 is now merged as main82da8da64 after clean1,786/1,786 hosted E2E and all other required checks. Production verification is pending. Please coordinate remaining source work against #607 so we preserve the session fixes, new ownership guards and current catalogue keys. #602 and #607 still require their own complete gates before release.

… that has it alone

Review on #605 pointed out that Next 16 (#602) bundles a React that already
fixes react/react#37584, and that the loader would reject it. Reading that
React (Next 16.3.6's react-dom-client, 19.3.0-canary-cbb046ab) shows where
React put the fix: in the replay's HostComponent case, right after the hooks
reset, the replayed fiber that is the current hydration parent puts the
parent back on its host parent and the cursor back on its node, or resumes
hydration if it had been suspended for an insertion below it.

The loader now injects exactly that code at exactly that site in Next
15.5's React (production and development, stable and experimental). The
hydration helpers it calls (popToNextHostParent,
tryToClaimNextHydratableInstance, resetHydrationState, popHydrationState)
are identical in 19.2 and 19.3. A React whose site already carries the fix
is returned unchanged, so #602 can land after this without touching it; a
React without the site still fails the build.

The test adds Next 16's case verbatim as a fixture that must come back
unchanged, checks the injection sits between the hooks reset and the
default branch, and parses the patched React.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K

Copy link
Copy Markdown
Owner Author

Thanks, both points were right. Both are addressed in cbb0dc45, which also carries main (#601) merged in.

Loop fix. I've adopted #607's lib/utils/stale-bundle-reload.ts and tests/a-failed-chunk-reloads-the-page.test.ts verbatim, with the storage-read loop fix and the five entry-point cases. The two branches now carry byte-identical versions of all six chunk and contrast paths, so #607 merges over them cleanly. The commit message only describes the loader; it did not list these two files, which went in with the same commit.

Loader vs Next 16. I read Next 16.3.6's own react-dom-client, React 19.3.0-canary-cbb046ab, from the npm tarball. The upstream fix sits in the replay's HostComponent case (case 5: resetHooksOnUnwind(…); followed by fiber === hydrationParentFiber && (isHydrating ? … : …)). The loader now:

  • injects exactly that code, at exactly that site, into Next 15.5's React (production and development, stable and experimental). The four hydration helpers it relies on are identical in 19.2 and 19.3;
  • returns a React whose site already carries the fix unchanged. Run against Next 16.3.6's production and development builds, it leaves both untouched, and Next 16's case is a verbatim fixture in tests/react-hydration-replay-fix.test.ts;
  • still fails the build on a React with no such site.

So #602 can land after this with no collision. next.config.mjs changes in hunks that don't overlap #602's, and once #602 is in, the loader and its rule are dead code to delete in any later PR. Until #602 has cleared its gates, production gets the fix from this PR.

Evidence for the first form of the backport (a rewind at the top of the function): a local production build ran the #418 hunt, 273 signed-in routes × 3 rounds at 4 in parallel, and got 0 of 819, against 7 of 819 unpatched. The React-19.3-shaped injection gets the same hunt on a fresh build before I merge.


Generated by Claude Code

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

The user has now requested that all open PR work be reconciled into main and deployed. The current main audit checkpoint is4123f13 (#608); production session/meal/page repairs from#601 are verified on82da. #602 carries the Next16 renderer fix and a default-false manual production-SQL apply guard. #607 carries repaired Inbox ownership, shared date hydration, localized controls, and the corrected marketplace test fixture (20,871 unit tests now pass).

I am reconciling these branches for release. Please preserve the newer main/607 session, Inbox, Modal, formatter, and cache guards when resolving overlaps. Pending SQL must remain unapplied; no production migration dispatch is authorized by this release coordination. Additional unique changes will be reviewed and integrated, with superseded alternatives recorded explicitly. Please keep finalaudit.md claims and prior failures intact.

tests/ordering-guards-fail-on-absence.test.ts holds every ordering
assertion to at()/between() from tests/helpers/source-order.ts, since a
bare indexOf answers -1 for an absent needle and passes. The placement
check now asserts that the fix follows the hooks reset directly and ends
before the default branch, with both needles required to be present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
@NewWorldVenture NewWorldVenture changed the title Page audit: backport React's hydration replay fix; a failed chunk reloads the page Page audit: backport React 19.3's hydration replay fix; a failed chunk reloads the page once Sep 27, 2026
@NewWorldVenture
NewWorldVenture marked this pull request as ready for review September 27, 2026 19:38
@NewWorldVenture
NewWorldVenture merged commit fda9dcf into main Sep 27, 2026
7 checks passed
NewWorldVenture pushed a commit that referenced this pull request Sep 27, 2026
Main's #605 test asserted the installed react-dom client did not already
carry react/react#37584's fix. Next 16.3.6 bundles React 19.3, which does,
so the test failed on this branch in CI. It now asserts the installed
React carries the fix and that the loader leaves it byte-identical, and
still exercises the backport on the same file with React's fix removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
NewWorldVenture pushed a commit that referenced this pull request Sep 27, 2026
NewWorldVenture pushed a commit that referenced this pull request Sep 27, 2026
Main's #605 test asserted the installed react-dom client did not already
carry react/react#37584's fix. Next 16.3.6 bundles React 19.3, which does,
so the test failed on this branch in CI. It now asserts the installed
React carries the fix and that the loader leaves it byte-identical, and
still exercises the backport on the same file with React's fix removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
NewWorldVenture pushed a commit that referenced this pull request Sep 27, 2026
Production at fda9dcf, signed out, all 393 routes at phone and desktop
(786 loads), and a local production build of the same source, signed in as
a parent and super admin with every migration to 0458 applied locally, the
same 786 loads. Signed in: 0 axe violations, 0 overflow, 0 page errors, 0
5xx. Production: every route gave the right answer; 30 loads had one
subresource answer 502 or time out through this sandbox's relay (which
logged its own tunnel failures to www.bubaly.com at the same minutes; the
deployment's chunks answered 200 90 of 90 times fetched directly), and every
one was clean on a serial retry.

The intermittent React #418 (the eight OWNER rows and the row a CI run had
opened on /dashboard/sleep) is closed by #605's backport of React 19.3's own
fix: the #418 hunt went from 7 of 819 to 0 of 819, and 0 of 270 on the nine
pages it had struck. The findings table records the backport, the chunk
failure that sat on the error page and its fix, the error page's contrast,
and this re-crawl; the rows #591 left "re-crawl pending" and three another
session left "not yet" (their fixes merged in #585/#603) are live and
re-crawled. Lanes B-F are released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture added a commit that referenced this pull request Sep 27, 2026
…ation-ld8bon

finalaudit: every page re-crawled after #605 — all 398 rows clean
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