survive session-bundle rebuild in chat, feeds, prefs, and age assurance
Review fixes (PR #11182 round 2). Client identity is no longer stable across token refresh - on web, a cross-tab sync rebuilds the bundle and disposes the old clients (whose fetch now throws) - so anything that captured a client at construction time must re-point at the live one: - MessagesEventBus and Convo get updateClient(); providers sync it via effect. No remount, so poll cursors and optimistic pendingMessages survive. - FeedAPI implementations get setClient(); the post-feed queryFn and pollLatest re-point page-held apis before fetching (Merge/Home apis are stateful across pages, so they cannot be rebuilt per fetch). - usePreferencesQuery applies fetched labeler dids to the live appview client (applyLabelersToClient, factored from session/moderation), restoring the old BskyAgent.getPreferences header side effect. - ageAssurance redirect overlay/dialog polling is mount-only with render-synced refs; a client swap mid-poll no longer latches the unmounted flag and strands the overlay. - Convo message-failure classification defers to lex's shouldRetry() instead of treating all status-less errors as recoverable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -178,18 +178,28 @@ function Inner() {
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const appviewClient = useAppviewClient()
|
||||
const polling = useRef(false)
|
||||
const unmounted = useRef(false)
|
||||
/*
|
||||
* The poll effect is mount-only so a session-bundle rebuild (web cross-tab
|
||||
* token sync, which swaps the appview client identity) does not restart the
|
||||
* flow or permanently latch it. Read the volatile values through refs kept
|
||||
* fresh each render so the next retry attempt picks up the current client.
|
||||
*/
|
||||
const clientRef = useRef(appviewClient)
|
||||
clientRef.current = appviewClient
|
||||
const hasSessionRef = useRef(hasSession)
|
||||
hasSessionRef.current = hasSession
|
||||
const openMetricFired = useRef(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const {close} = useRedirectOverlayContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (polling.current) return
|
||||
let cancelled = false
|
||||
|
||||
polling.current = true
|
||||
|
||||
ax.metric('ageAssurance:redirectDialogOpen', {})
|
||||
if (!openMetricFired.current) {
|
||||
openMetricFired.current = true
|
||||
ax.metric('ageAssurance:redirectDialogOpen', {})
|
||||
}
|
||||
|
||||
wait(
|
||||
3e3,
|
||||
@@ -197,10 +207,12 @@ function Inner() {
|
||||
5,
|
||||
() => true,
|
||||
async () => {
|
||||
if (!hasSession) return
|
||||
if (unmounted.current) return
|
||||
if (!hasSessionRef.current) return
|
||||
if (cancelled) return
|
||||
|
||||
const data = await refetchAgeAssuranceServerState({appviewClient})
|
||||
const data = await refetchAgeAssuranceServerState({
|
||||
appviewClient: clientRef.current,
|
||||
})
|
||||
|
||||
if (data?.state.status !== 'assured') {
|
||||
throw new Error(
|
||||
@@ -215,23 +227,24 @@ function Inner() {
|
||||
)
|
||||
.then(async data => {
|
||||
if (!data) return
|
||||
if (!hasSession) return
|
||||
if (unmounted.current) return
|
||||
if (!hasSessionRef.current) return
|
||||
if (cancelled) return
|
||||
|
||||
setSuccess(true)
|
||||
|
||||
ax.metric('ageAssurance:redirectDialogSuccess', {})
|
||||
})
|
||||
.catch(() => {
|
||||
if (unmounted.current) return
|
||||
if (cancelled) return
|
||||
setError(true)
|
||||
ax.metric('ageAssurance:redirectDialogFail', {})
|
||||
})
|
||||
|
||||
return () => {
|
||||
unmounted.current = true
|
||||
cancelled = true
|
||||
}
|
||||
}, [ax, hasSession, appviewClient])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user