You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #707 (Fixes #708) is merged but insufficient. Uncaught QuotaExceededError on tsr-scroll-restoration-v1_3 still occurs in production on the current web bundle, tripping React's error boundary during normal session chat use.
This is a follow-up to the scroll-restoration quota chain:
Hard-reset + sync in-memory cache via scrollRestorationCache.set
The #707 fix introduced a temporary unwrap of sessionStorage.setItem that creates a race: TanStack can call scrollRestorationCache.set (which calls native setItem) while the guard is bypassed.
Evidence (production, not stale cache)
Observed on https://hapi.tail9944ee.ts.net after driver rebuild serving bundle index-BoTBCCy9.js (bundle contains __hapiScrollRestorationGuard — guard is installed).
Console log excerpt (session chat, 2026-05-27):
Uncaught QuotaExceededError: Failed to execute 'setItem' on 'Storage':
Setting the value of 'tsr-scroll-restoration-v1_3' exceeded the quota.
at Object.set (index-BoTBCCy9.js:10:94081)
at s (index-BoTBCCy9.js:10:95765)
...
setTimeout
scroll
Second occurrence (caught by error boundary / React DevTools hook):
QuotaExceededError: ... 'tsr-scroll-restoration-v1_3' ...
at Object.set (index-BoTBCCy9.js:10:94081)
at Object.fn (index-BoTBCCy9.js:10:96489)
at Set.forEach
at lR.emit
at ref
Interpretation of stacks:
Object.set = TanStack scrollRestorationCache.set in @tanstack/router-core/src/scroll-restoration.ts (line 71: safeSessionStorage.setItem(storageKey, JSON.stringify(state))).
Problem: While storage.setItem === originalSetItem, any concurrent TanStack scroll write (scrollRestorationCache.set from throttled scroll handler or onRendered) hits unwrapped native setItem. On quota exhaustion that throw is uncaught — it never enters wrappedSetItem's recovery logic.
The existing unit test hard reset calls scrollRestorationCache through unwrapped setItem documents that unwrap is intentional for in-memory sync, but there is no protection against concurrent TanStack writes during the unwrap window.
Additional unit test added locally (not yet upstream):
exposes unwrap window where concurrent TanStack scroll writes can throw uncaught — simulates TanStack calling setItem while writeScrollRestorationCache has unwrapped; demonstrates uncaught throw path.
Why #707 manual test plan item was never satisfied
Manual: long session chat, scroll, navigate until storage is stressed; confirm no uncaught setItem / no white-screen error boundary
That step was unchecked at merge. Playwright stress tests against local + production hub with artificial sessionStorage fill often pass (guard handles wrapped-path recovery), but the race is intermittent and matches real operator profiles with accumulated storage + active scrolling during recovery.
Suggested fix directions (for follow-up PR)
Do not temporarily assign storage.setItem = originalSetItem. Options (pick one, verify with tests):
Patch scrollRestorationCache.set directly — update in-memory state without persisting, then persist once through the wrapped setItem (or call originalSetItem only from inside wrappedSetItem where recovery already handles failures).
Re-entrant lock / depth counter — if guard recovery is in progress, route all scrollRestorationCache.set persistence through wrappedSetItem even when syncing RAM (never expose native setItem on the shared sessionStorage object).
Reduce concurrent writers — optional: disable or debounce scroll restoration persist while hard-reset runs (harder to reason about; prefer fixing unwrap).
Reproduction / verification checklist for fix PR
# Unit tests (extend scrollStorageGuard.test.ts)cd web && bun run test src/lib/scrollStorageGuard.test.ts
# Playwright stress (repo root; uses system Chrome on Linux)
HAPI_URL=https://your-hub HAPI_ACCESS_TOKEN=... \
node scripts/dev/scroll-quota-repro-playwright.mjs \
--session <session-id> --fill-mb 4.5
# Must add: concurrent-write race test (see test name above)# Must pass: no pageerror/console QuotaExceededError on tsr-scroll-restoration-v1_3
Manual (required): Long-lived browser profile with stressed sessionStorage, session chat open, scroll + navigate while storage near quota — confirm zero uncaught QuotaExceededError and no React error boundary.
Summary
PR #707 (
Fixes #708) is merged but insufficient. UncaughtQuotaExceededErrorontsr-scroll-restoration-v1_3still occurs in production on the current web bundle, tripping React's error boundary during normal session chat use.This is a follow-up to the scroll-restoration quota chain:
sessionStoragequota exhaustion on TanStack scroll key crashes SPAgetScrollRestorationKey→ pathname to slow cache growthscrollRestorationCache.setThe #707 fix introduced a temporary unwrap of
sessionStorage.setItemthat creates a race: TanStack can callscrollRestorationCache.set(which calls nativesetItem) while the guard is bypassed.Evidence (production, not stale cache)
Observed on
https://hapi.tail9944ee.ts.netafter driver rebuild serving bundleindex-BoTBCCy9.js(bundle contains__hapiScrollRestorationGuard— guard is installed).Console log excerpt (session chat, 2026-05-27):
Second occurrence (caught by error boundary / React DevTools hook):
Interpretation of stacks:
Object.set= TanStackscrollRestorationCache.setin@tanstack/router-core/src/scroll-restoration.ts(line 71:safeSessionStorage.setItem(storageKey, JSON.stringify(state))).setTimeout→scroll= throttleddocumentscroll listener insetupScrollRestoration(100ms throttle).emit→ref= routeronRenderedsubscriber also callingscrollRestorationCache.setto mark location seen.These paths call
scrollRestorationCache.setdirectly, not only through the wrappedsessionStorage.setItementry point.HAR capture confirms the same JS bundle (
index-BoTBCCy9.js) was loaded for the failing session URL.Root cause
web/src/lib/scrollStorageGuard.ts—writeScrollRestorationCache():Called from:
wrappedSetItem, after trimmed write succeeds)hardResetScrollRestorationPersistedState)Problem: While
storage.setItem === originalSetItem, any concurrent TanStack scroll write (scrollRestorationCache.setfrom throttled scroll handler oronRendered) hits unwrapped nativesetItem. On quota exhaustion that throw is uncaught — it never enterswrappedSetItem's recovery logic.The existing unit test
hard reset calls scrollRestorationCache through unwrapped setItemdocuments that unwrap is intentional for in-memory sync, but there is no protection against concurrent TanStack writes during the unwrap window.Additional unit test added locally (not yet upstream):
exposes unwrap window where concurrent TanStack scroll writes can throw uncaught— simulates TanStack callingsetItemwhilewriteScrollRestorationCachehas unwrapped; demonstrates uncaught throw path.Why #707 manual test plan item was never satisfied
PR #707 test plan included:
That step was unchecked at merge. Playwright stress tests against local + production hub with artificial
sessionStoragefill often pass (guard handles wrapped-path recovery), but the race is intermittent and matches real operator profiles with accumulated storage + active scrolling during recovery.Suggested fix directions (for follow-up PR)
Do not temporarily assign
storage.setItem = originalSetItem. Options (pick one, verify with tests):Patch
scrollRestorationCache.setdirectly — update in-memory state without persisting, then persist once through the wrappedsetItem(or calloriginalSetItemonly from insidewrappedSetItemwhere recovery already handles failures).Re-entrant lock / depth counter — if guard recovery is in progress, route all
scrollRestorationCache.setpersistence throughwrappedSetItemeven when syncing RAM (never expose nativesetItemon the sharedsessionStorageobject).Upstream belt-and-suspenders — Upgrade
@tanstack/react-router— upstream already has built-in scroll-restoration quota fix #683 tracks upgrading@tanstack/react-router; upstream may wrapsetItemin try/catch, but HAPI should not rely on that alone given the unwrap race.Reduce concurrent writers — optional: disable or debounce scroll restoration persist while hard-reset runs (harder to reason about; prefer fixing unwrap).
Reproduction / verification checklist for fix PR
Manual (required): Long-lived browser profile with stressed
sessionStorage, session chat open, scroll + navigate while storage near quota — confirm zero uncaughtQuotaExceededErrorand no React error boundary.Related
tsr-scroll-restoration-v1_3after accumulating many session/file routes #611 — original quota crash@tanstack/react-router— upstream already has built-in scroll-restoration quota fix #683 — router upgrade (optional additional hardening)Files
web/src/lib/scrollStorageGuard.ts— bug location (writeScrollRestorationCache)web/src/main.tsx—installScrollRestorationGuard()at bootstrap (guard IS installed; not a boot-order bug)node_modules/@tanstack/router-core/src/scroll-restoration.ts— TanStack persist + scroll listenersweb/src/lib/scrollStorageGuard.test.ts— existing + proposed race test