[Fix Logouts] Persist accounts synchronously (#9109)

* Make persisting synchronous

* Initialize later so persisted is filled
This commit is contained in:
dan
2025-10-03 17:02:52 +01:00
committed by GitHub
parent b68f80050c
commit 885356256e
+62 -39
View File
@@ -14,7 +14,7 @@ import {
createAgentAndResume, createAgentAndResume,
sessionAccountToSession, sessionAccountToSession,
} from './agent' } from './agent'
import {getInitialState, reducer} from './reducer' import {type Action, getInitialState, reducer, type State} from './reducer'
export {isSignupQueued} from './util' export {isSignupQueued} from './util'
import {addSessionDebugLog} from './logging' import {addSessionDebugLog} from './logging'
@@ -46,13 +46,51 @@ const ApiContext = React.createContext<SessionApiContext>({
}) })
ApiContext.displayName = 'SessionApiContext' ApiContext.displayName = 'SessionApiContext'
export function Provider({children}: React.PropsWithChildren<{}>) { class SessionStore {
const cancelPendingTask = useOneTaskAtATime() private state: State
const [state, dispatch] = React.useReducer(reducer, null, () => { private listeners = new Set<() => void>()
constructor() {
// Careful: By the time this runs, `persisted` needs to already be filled.
const initialState = getInitialState(persisted.get('session').accounts) const initialState = getInitialState(persisted.get('session').accounts)
addSessionDebugLog({type: 'reducer:init', state: initialState}) addSessionDebugLog({type: 'reducer:init', state: initialState})
return initialState this.state = initialState
}) }
getState = (): State => {
return this.state
}
subscribe = (listener: () => void) => {
this.listeners.add(listener)
return () => {
this.listeners.delete(listener)
}
}
dispatch = (action: Action) => {
const nextState = reducer(this.state, action)
this.state = nextState
// Persist synchronously without waiting for the React render cycle.
if (nextState.needsPersist) {
nextState.needsPersist = false
const persistedData = {
accounts: nextState.accounts,
currentAccount: nextState.accounts.find(
a => a.did === nextState.currentAgentState.did,
),
}
addSessionDebugLog({type: 'persisted:broadcast', data: persistedData})
persisted.write('session', persistedData)
}
this.listeners.forEach(listener => listener())
}
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const cancelPendingTask = useOneTaskAtATime()
const [store] = React.useState(() => new SessionStore())
const state = React.useSyncExternalStore(store.subscribe, store.getState)
const onAgentSessionChange = React.useCallback( const onAgentSessionChange = React.useCallback(
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
@@ -60,7 +98,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
emitSessionDropped() emitSessionDropped()
} }
dispatch({ store.dispatch({
type: 'received-agent-event', type: 'received-agent-event',
agent, agent,
refreshedAccount, refreshedAccount,
@@ -68,7 +106,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
sessionEvent, sessionEvent,
}) })
}, },
[], [store],
) )
const createAccount = React.useCallback<SessionApiContext['createAccount']>( const createAccount = React.useCallback<SessionApiContext['createAccount']>(
@@ -84,7 +122,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (signal.aborted) { if (signal.aborted) {
return return
} }
dispatch({ store.dispatch({
type: 'switched-to-account', type: 'switched-to-account',
newAgent: agent, newAgent: agent,
newAccount: account, newAccount: account,
@@ -92,7 +130,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logger.metric('account:create:success', metrics, {statsig: true}) logger.metric('account:create:success', metrics, {statsig: true})
addSessionDebugLog({type: 'method:end', method: 'createAccount', account}) addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
}, },
[onAgentSessionChange, cancelPendingTask], [store, onAgentSessionChange, cancelPendingTask],
) )
const login = React.useCallback<SessionApiContext['login']>( const login = React.useCallback<SessionApiContext['login']>(
@@ -107,7 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (signal.aborted) { if (signal.aborted) {
return return
} }
dispatch({ store.dispatch({
type: 'switched-to-account', type: 'switched-to-account',
newAgent: agent, newAgent: agent,
newAccount: account, newAccount: account,
@@ -119,7 +157,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
addSessionDebugLog({type: 'method:end', method: 'login', account}) addSessionDebugLog({type: 'method:end', method: 'login', account})
}, },
[onAgentSessionChange, cancelPendingTask], [store, onAgentSessionChange, cancelPendingTask],
) )
const logoutCurrentAccount = React.useCallback< const logoutCurrentAccount = React.useCallback<
@@ -128,7 +166,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logContext => { logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'}) addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask() cancelPendingTask()
dispatch({ store.dispatch({
type: 'logged-out-current-account', type: 'logged-out-current-account',
}) })
logger.metric( logger.metric(
@@ -138,7 +176,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
addSessionDebugLog({type: 'method:end', method: 'logout'}) addSessionDebugLog({type: 'method:end', method: 'logout'})
}, },
[cancelPendingTask], [store, cancelPendingTask],
) )
const logoutEveryAccount = React.useCallback< const logoutEveryAccount = React.useCallback<
@@ -147,7 +185,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logContext => { logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'}) addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask() cancelPendingTask()
dispatch({ store.dispatch({
type: 'logged-out-every-account', type: 'logged-out-every-account',
}) })
logger.metric( logger.metric(
@@ -157,7 +195,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
addSessionDebugLog({type: 'method:end', method: 'logout'}) addSessionDebugLog({type: 'method:end', method: 'logout'})
}, },
[cancelPendingTask], [store, cancelPendingTask],
) )
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
@@ -176,14 +214,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (signal.aborted) { if (signal.aborted) {
return return
} }
dispatch({ store.dispatch({
type: 'switched-to-account', type: 'switched-to-account',
newAgent: agent, newAgent: agent,
newAccount: account, newAccount: account,
}) })
addSessionDebugLog({type: 'method:end', method: 'resumeSession', account}) addSessionDebugLog({type: 'method:end', method: 'resumeSession', account})
}, },
[onAgentSessionChange, cancelPendingTask], [store, onAgentSessionChange, cancelPendingTask],
) )
const partialRefreshSession = React.useCallback< const partialRefreshSession = React.useCallback<
@@ -193,7 +231,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const signal = cancelPendingTask() const signal = cancelPendingTask()
const {data} = await agent.com.atproto.server.getSession() const {data} = await agent.com.atproto.server.getSession()
if (signal.aborted) return if (signal.aborted) return
dispatch({ store.dispatch({
type: 'partial-refresh-session', type: 'partial-refresh-session',
accountDid: agent.session!.did, accountDid: agent.session!.did,
patch: { patch: {
@@ -201,7 +239,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
emailAuthFactor: data.emailAuthFactor, emailAuthFactor: data.emailAuthFactor,
}, },
}) })
}, [state, cancelPendingTask]) }, [store, state, cancelPendingTask])
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => { account => {
@@ -211,34 +249,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
account, account,
}) })
cancelPendingTask() cancelPendingTask()
dispatch({ store.dispatch({
type: 'removed-account', type: 'removed-account',
accountDid: account.did, accountDid: account.did,
}) })
addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) addSessionDebugLog({type: 'method:end', method: 'removeAccount', account})
}, },
[cancelPendingTask], [store, cancelPendingTask],
) )
React.useEffect(() => {
if (state.needsPersist) {
state.needsPersist = false
const persistedData = {
accounts: state.accounts,
currentAccount: state.accounts.find(
a => a.did === state.currentAgentState.did,
),
}
addSessionDebugLog({type: 'persisted:broadcast', data: persistedData})
persisted.write('session', persistedData)
}
}, [state])
React.useEffect(() => { React.useEffect(() => {
return persisted.onUpdate('session', nextSession => { return persisted.onUpdate('session', nextSession => {
const synced = nextSession const synced = nextSession
addSessionDebugLog({type: 'persisted:receive', data: synced}) addSessionDebugLog({type: 'persisted:receive', data: synced})
dispatch({ store.dispatch({
type: 'synced-accounts', type: 'synced-accounts',
syncedAccounts: synced.accounts, syncedAccounts: synced.accounts,
syncedCurrentDid: synced.currentAccount?.did, syncedCurrentDid: synced.currentAccount?.did,
@@ -262,7 +285,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
} }
}) })
}, [state, resumeSession]) }, [store, state, resumeSession])
const stateContext = React.useMemo( const stateContext = React.useMemo(
() => ({ () => ({