Fix Supabase client idle-freeze that bricks the page after tab suspension#38
Merged
Merged
Conversation
When a user leaves the tab alone for a while, the Supabase JS client could enter a 'zombie' state (a known upstream bug, supabase/supabase#36046). After that, every query and auth call would hang forever: leaderboards would spin indefinitely, and score submissions would silently do nothing. The only recovery was a full page refresh — which cost the user their unsubmitted score. The console error users saw ('Unchecked runtime.lastError: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received') is emitted by browser extensions whose background scripts get suspended, which is the same class of event that triggers the Supabase deadlock. Changes: - Upgrade @supabase/supabase-js from ^2.99.1 to ^2.103.3 to pick up upstream auth-lock fixes (PRs #2228, #2214, #2239 upstream). - Rewrite src/lib/supabaseClient.js to export a Proxy over a mutable internal client instance. This lets us fully rebuild the client when it deadlocks without breaking any consumers that captured the supabase import. - Add checkAndRecoverHealth(): probes auth.getSession() with a 3s timeout; if it hangs, recreates the client entirely. Preserves any onAuthStateChange listeners across recreations. - App.js: replace the naive 60s getSession keep-alive with a comprehensive recovery effect that runs health checks on visibilitychange, window focus, bfcache pageshow, and on a 60s interval. Auto-refetches leaderboards and flushes any queued submit after recovery. - useLeaderboardStore: wrap submitScore reads and writes in the same timeout + retry + health-recover logic as fetches. On persistent failure, queue the submission to localStorage so the score is NEVER lost — it auto-retries on focus/reconnect and on next load. Surface a user-facing message ('Connection issue — score saved and will submit automatically') instead of doing nothing. - useAuthStore: timeout-wrap the initial getSession() call so a wedged persisted auth state at boot can't freeze the app. - Add unit tests for the Proxy indirection, the getSession-hang recovery path, listener rebinding on recreate, submit-retry queueing, and queue flush behavior. Co-authored-by: Chris Sanchez <cwsanchez@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… metadata On protected Vercel preview deployments, fetching manifest.json returns 401 Unauthorized because the browser omits cookies on the uncredentialed manifest request, so Vercel's SSO wall rejects it. Adding crossorigin="use-credentials" tells the browser to include the _vercel_sso_nonce cookie, which clears the 401. No effect on public / production deployments (manifest is same-origin there). See: vercel/next.js#62866 Also replace the CRA boilerplate manifest (which still said 'Create React App Sample' / '#000000' theme) with proper Stop the Clock! metadata matching the in-app theme. Co-authored-by: Chris Sanchez <cwsanchez@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bugs
1. Backend stops responding after tab idle
When a user leaves the tab alone for a while, the backend stops responding until the page is refreshed:
Unchecked runtime.lastError: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received.2.
GET /manifest.json 401 Unauthorizedon every page loadSeparate issue surfaced during testing — every page load / refresh on the Vercel preview deployment logs two console errors for
manifest.jsonreturning 401.Root causes
For the idle freeze
Supabase JS client idle-freeze. The
@supabase/supabase-jsclient can enter a "zombie" state after tab suspension. Its internal auth lock (navigator.locksor the library's fallback) fails to release, and every subsequent.from(...),.auth.getSession(),.auth.signIn(...), etc. hangs forever — the HTTP request never even leaves the client. This is a well-known upstream bug (see supabase/supabase#36046, supabase-js#2013, supabase-js PR #2228).The
"Unchecked runtime.lastError"console line is a red herring — it's emitted by Chrome browser extensions (MetaMask, password managers, Grammarly, etc.) when their background service worker gets suspended. It's not from this app, but it's a strong signal that the user's browser just went through an idle/suspension cycle, which is exactly what puts the Supabase client into the deadlocked state.The old mitigations in the repo (no-op auth
lock, 60sgetSessionkeep-alive,forceReconnectthat only cleared channels + refreshed the session) weren't enough because they didn't actually tear down and rebuild the underlying client instance.submitScore()also had no timeout/retry wrapping — so once the client was wedged, submitting just sat there.For the manifest.json 401
This is a documented Vercel-preview-deployment quirk: preview URLs are wrapped by Vercel's SSO auth wall, which requires a
_vercel_sso_noncecookie to return assets. By default, browsers fetchmanifest.jsonwithout credentials, so the cookie isn't sent and Vercel returns its auth-wall HTML with a 401 status. Confirmed by the response:Content-Type: text/html, 14.6 kB size,Server: Vercel,Set-Cookie: _vercel_sso_nonce=.... The<link rel="manifest">needscrossorigin="use-credentials"so the browser sends cookies. (Does nothing to public / production deployments since the manifest is same-origin there.)The fix
Idle-freeze fix
@supabase/supabase-jsfrom^2.99.1→^2.103.3to pick up upstream auth-lock fixes.src/lib/supabaseClient.jsto export aProxyover a mutable internal client instance. This lets us fully rebuild the client at runtime without invalidating any consumers that captured thesupabaseimport.checkAndRecoverHealth()— a 3s-timeoutauth.getSession()probe. If it hangs, the client is rebuilt end-to-end.onAuthStateChangelisteners are transparently rebound onto the new client.App.js— replace the naive 60s keep-alive with a comprehensive recovery effect that runs health checks onvisibilitychange,focus,pageshow(for bfcache restores), and on a 60s interval.useLeaderboardStore.submitScorewith the same timeout + retry wrapper used by fetches. On persistent failure, queue the submission tolocalStorageso the score is never silently lost — it auto-retries on focus/reconnect, on the next healthy request, and on next page load.Connection issue — score saved and will submit automatically.useAuthStore.initializeso a wedged persisted auth state at boot can't freeze the app.Manifest 401 fix
public/index.html: addcrossorigin="use-credentials"to the<link rel="manifest">. One-liner that makes the browser send cookies with the manifest request, which eliminates the 401 on Vercel preview deployments.public/manifest.json: while in there, replace the stale CRA boilerplate ("Create React App Sample",theme_color: "#000000") with the actual Stop the Clock! metadata matching the in-app theme.Tests
Added 9 new unit tests (12 total in the suite, all green):
src/lib/supabaseClient.test.js— directly simulates the bug with a mockedgetSessionthat never resolves, and asserts the client is rebuilt:src/stores/useLeaderboardStore.test.js— asserts that failed submissions are queued tolocalStorage, cleared on success, and flushable:Manual smoke tests
Idle-freeze recovery plumbing — Start/Stop timer works; visibility/focus/pageshow handlers run cleanly; no JS errors in console:
App loads cleanly after Proxy-based client refactor
No JS errors in console (only the expected network errors to the placeholder Supabase URL)
visibilitychange / focus / pageshow event handlers run cleanly, app remains responsive
Manifest 401 fix — hard-refreshed the page after the fix; no manifest errors in console,
manifest.jsonreturns 200 OK, and Chrome parses the correct app identity:Console shows no manifest errors after the fix
manifest.json now returns 200 OK
Chrome correctly parses the manifest showing
Verification notes for the deployed app
After merging, a user who experiences the freeze should now see one of:
Connection issue — score saved and will submit automatically.The score is written tolocalStorage(key:stc-pending-submit-v1) and auto-retries on next recovery / on next page load. They will no longer lose a score.The
manifest.json 401errors will disappear from the console on every deployment (preview and production).Risk assessment
supabase.from(...),supabase.auth.*, etc. call path unchanged at the call-site. The proxy forwards method lookups to the current live client, so no other files needed to be touched.localStorageand is safe.crossorigin="use-credentials"attribute is a no-op for same-origin manifests (public / production deployments), so there's no risk of it breaking non-preview environments.To show artifacts inline, enable in settings.