fix(provider): keep the session store alive across a remount - #119
Merged
Conversation
AuthProvider created the session store in useMemo but destroyed it in an effect cleanup. Those have different lifetimes: React can run mount, cleanup, mount against the same memoized value, which StrictMode does on every mount and Activity does whenever a hidden tree is shown again. destroy() is terminal, so the remounted provider held a store that refused every update, and refreshSession returned early before it could clear loading. Any app rendering the provider inside StrictMode, which is what the Vite template ships, stayed on loading forever, signed out or not. The provider no longer destroys the store. useSyncExternalStore removes its own listener on unmount and the store owns no timers, so it is reclaimed with the component. destroy() stays on the store for bindings that genuinely own its lifetime.
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.
Problem
AuthProvidercreates the session store inuseMemobut destroys it in auseEffectcleanup. Those have different lifetimes. React can run mount, cleanup, mount against the same memoized value: StrictMode does it on every mount, and Activity does it whenever a hidden tree is shown again.destroy()is terminal by design, so the remounted provider held a store that refused every update.refreshSessionthen returned early at itsdestroyedguard before it could setloading: false, andloadingstayedtrueforever.Any app rendering the provider inside
StrictMode, which is whattemplates/web/react-viteships, never left its loading state. That applies to an authenticated session as much as an anonymous one, so it was not limited to signed-out first loads.Found while running the react-vite and express templates against local
mainof every repo. The app sat on "Checking your session..." indefinitely.Fix
The provider no longer destroys the store from its effect cleanup.
Nothing leaks by skipping it.
useSyncExternalStoreremoves its own listener when the provider unmounts, and the store owns no timers or subscriptions, so it is reclaimed with the component. A refresh still in flight resolves into a store nobody observes.destroy()stays on the store. Its terminal semantics are intentional and covered byauthSession.test.ts, and a binding that genuinely owns the store's lifetime still needs it.The store's contract was sound, so the fix is in the binding that misused it rather than in
createAuthSessionor in the adapter's response code.Notes
The 400 that
@seamless-auth/corereturns for a missingseamless-accesscookie is arguably better as a 401, since an absent cookie is the ordinary anonymous case. It is not what broke this, and changing it is a cross-repo contract change, so it is left alone here.No public API change. The session store is not exported from
src/index.ts, anddist/index.d.tsis unchanged.The bug is unreleased:
src/sessiondoes not exist inv0.6.0, so no published version is affected. Rather than add a changeset describing a bug no user ever saw, the pendingnine-pans-shake.mdchangeset that introduced the store gained a paragraph on the remount behavior.Verification
Both new tests fail on the previous behavior and pass on this branch.
npm run typecheck,npm run lint,npm run format:check: cleannpm test -- --runInBand: 271 passed, 31 suites, coverage thresholds metnpm run build,npm run check-npm-build: cleanAlso exercised end to end against a local stack (auth API from source, express template, react-vite template) with
StrictModein place: registration, email OTP, and a signed-in session surviving a full page reload.Activityis the same mechanism and the same fix, but only theStrictModepath was exercised.