[Session] Use flag on state for persistence (#3793)

* Move isInitialLoad and isSwitchingAccounts out of main state

* Remove spreads, order object keys

* Track need to persist on state object

* Reoder state variables
zio/stable
dan 2024-05-01 17:26:33 +01:00 committed by GitHub
parent a6061489ff
commit df9af92eb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 48 deletions

View File

@ -26,7 +26,6 @@ export type {SessionAccount} from '#/state/session/types'
import { import {
SessionAccount, SessionAccount,
SessionApiContext, SessionApiContext,
SessionState,
SessionStateContext, SessionStateContext,
} from '#/state/session/types' } from '#/state/session/types'
@ -61,45 +60,44 @@ function __getAgent() {
return __globalAgent return __globalAgent
} }
type State = {
accounts: SessionStateContext['accounts']
currentAccount: SessionStateContext['currentAccount']
needsPersist: boolean
}
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const isDirty = React.useRef(false) const [isInitialLoad, setIsInitialLoad] = React.useState(true)
const [state, setState] = React.useState<SessionState>({ const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
isInitialLoad: true, const [state, setState] = React.useState<State>({
isSwitchingAccounts: false,
accounts: persisted.get('session').accounts, accounts: persisted.get('session').accounts,
currentAccount: undefined, // assume logged out to start currentAccount: undefined, // assume logged out to start
needsPersist: false,
}) })
const setStateAndPersist = React.useCallback(
(fn: (prev: SessionState) => SessionState) => {
isDirty.current = true
setState(fn)
},
[setState],
)
const upsertAccount = React.useCallback( const upsertAccount = React.useCallback(
(account: SessionAccount, expired = false) => { (account: SessionAccount, expired = false) => {
setStateAndPersist(s => { setState(s => {
return { return {
...s,
currentAccount: expired ? undefined : account,
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
currentAccount: expired ? undefined : account,
needsPersist: true,
} }
}) })
}, },
[setStateAndPersist], [setState],
) )
const clearCurrentAccount = React.useCallback(() => { const clearCurrentAccount = React.useCallback(() => {
logger.warn(`session: clear current account`) logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT __globalAgent = PUBLIC_BSKY_AGENT
configureModerationForGuest() configureModerationForGuest()
setStateAndPersist(s => ({ setState(s => ({
...s, accounts: s.accounts,
currentAccount: undefined, currentAccount: undefined,
needsPersist: true,
})) }))
}, [setStateAndPersist]) }, [setState])
const createPersistSessionHandler = React.useCallback( const createPersistSessionHandler = React.useCallback(
( (
@ -260,19 +258,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
async logContext => { async logContext => {
logger.debug(`session: logout`) logger.debug(`session: logout`)
clearCurrentAccount() clearCurrentAccount()
setStateAndPersist(s => { setState(s => {
return { return {
...s,
accounts: s.accounts.map(a => ({ accounts: s.accounts.map(a => ({
...a, ...a,
refreshJwt: undefined, refreshJwt: undefined,
accessJwt: undefined, accessJwt: undefined,
})), })),
currentAccount: s.currentAccount,
needsPersist: true,
} }
}) })
logEvent('account:loggedOut', {logContext}) logEvent('account:loggedOut', {logContext})
}, },
[clearCurrentAccount, setStateAndPersist], [clearCurrentAccount, setState],
) )
const initSession = React.useCallback<SessionApiContext['initSession']>( const initSession = React.useCallback<SessionApiContext['initSession']>(
@ -397,10 +396,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} catch (e) { } catch (e) {
logger.error(`session: resumeSession failed`, {message: e}) logger.error(`session: resumeSession failed`, {message: e})
} finally { } finally {
setState(s => ({ setIsInitialLoad(false)
...s,
isInitialLoad: false,
}))
} }
}, },
[initSession], [initSession],
@ -408,21 +404,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => { account => {
setStateAndPersist(s => { setState(s => {
return { return {
...s,
accounts: s.accounts.filter(a => a.did !== account.did), accounts: s.accounts.filter(a => a.did !== account.did),
currentAccount: s.currentAccount,
needsPersist: true,
} }
}) })
}, },
[setStateAndPersist], [setState],
) )
const updateCurrentAccount = React.useCallback< const updateCurrentAccount = React.useCallback<
SessionApiContext['updateCurrentAccount'] SessionApiContext['updateCurrentAccount']
>( >(
account => { account => {
setStateAndPersist(s => { setState(s => {
const currentAccount = s.currentAccount const currentAccount = s.currentAccount
// ignore, should never happen // ignore, should never happen
@ -443,38 +440,38 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
return { return {
...s,
currentAccount: updatedAccount,
accounts: [ accounts: [
updatedAccount, updatedAccount,
...s.accounts.filter(a => a.did !== currentAccount.did), ...s.accounts.filter(a => a.did !== currentAccount.did),
], ],
currentAccount: updatedAccount,
needsPersist: true,
} }
}) })
}, },
[setStateAndPersist], [setState],
) )
const selectAccount = React.useCallback<SessionApiContext['selectAccount']>( const selectAccount = React.useCallback<SessionApiContext['selectAccount']>(
async (account, logContext) => { async (account, logContext) => {
setState(s => ({...s, isSwitchingAccounts: true})) setIsSwitchingAccounts(true)
try { try {
await initSession(account) await initSession(account)
setState(s => ({...s, isSwitchingAccounts: false})) setIsSwitchingAccounts(false)
logEvent('account:loggedIn', {logContext, withPassword: false}) logEvent('account:loggedIn', {logContext, withPassword: false})
} catch (e) { } catch (e) {
// reset this in case of error // reset this in case of error
setState(s => ({...s, isSwitchingAccounts: false})) setIsSwitchingAccounts(false)
// but other listeners need a throw // but other listeners need a throw
throw e throw e
} }
}, },
[setState, initSession], [initSession],
) )
React.useEffect(() => { React.useEffect(() => {
if (isDirty.current) { if (state.needsPersist) {
isDirty.current = false state.needsPersist = false
persisted.write('session', { persisted.write('session', {
accounts: state.accounts, accounts: state.accounts,
currentAccount: state.currentAccount, currentAccount: state.currentAccount,
@ -533,10 +530,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
clearCurrentAccount() clearCurrentAccount()
} }
setState(s => ({ setState(() => ({
...s,
accounts: session.accounts, accounts: session.accounts,
currentAccount: selectedAccount, currentAccount: selectedAccount,
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
})) }))
}) })
}, [state, setState, clearCurrentAccount, initSession]) }, [state, setState, clearCurrentAccount, initSession])
@ -544,9 +541,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const stateContext = React.useMemo( const stateContext = React.useMemo(
() => ({ () => ({
...state, ...state,
isInitialLoad,
isSwitchingAccounts,
hasSession: !!state.currentAccount, hasSession: !!state.currentAccount,
}), }),
[state], [state, isInitialLoad, isSwitchingAccounts],
) )
const api = React.useMemo( const api = React.useMemo(

View File

@ -3,13 +3,11 @@ import {PersistedAccount} from '#/state/persisted'
export type SessionAccount = PersistedAccount export type SessionAccount = PersistedAccount
export type SessionState = { export type SessionStateContext = {
isInitialLoad: boolean
isSwitchingAccounts: boolean
accounts: SessionAccount[] accounts: SessionAccount[]
currentAccount: SessionAccount | undefined currentAccount: SessionAccount | undefined
} isInitialLoad: boolean
export type SessionStateContext = SessionState & { isSwitchingAccounts: boolean
hasSession: boolean hasSession: boolean
} }
export type SessionApiContext = { export type SessionApiContext = {