[Fix Logouts] Persist accounts synchronously (#9109)
* Make persisting synchronous * Initialize later so persisted is filled
This commit is contained in:
+62
-39
@@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user