[Session V2] Persist agent changes automatically
This commit is contained in:
+81
-91
@@ -8,11 +8,7 @@ import {logEvent} from '#/lib/statsig/statsig'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {
|
import {SessionApiContext, SessionStateContext} from '#/state/session/types'
|
||||||
SessionAccount,
|
|
||||||
SessionApiContext,
|
|
||||||
SessionStateContext,
|
|
||||||
} from '#/state/session/types'
|
|
||||||
import {
|
import {
|
||||||
agentToSessionAccount,
|
agentToSessionAccount,
|
||||||
configureModerationForAccount,
|
configureModerationForAccount,
|
||||||
@@ -62,12 +58,12 @@ const ApiContext = React.createContext<SessionApiContext>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const isDirty = React.useRef(false)
|
const [state, setState] = React.useState(() => ({
|
||||||
const [currentAgent, setCurrentAgent] =
|
accounts: persisted.get('session').accounts,
|
||||||
React.useState<BskyAgent>(INITIAL_AGENT)
|
currentAgent: INITIAL_AGENT,
|
||||||
const [accounts, setAccounts] = React.useState<SessionAccount[]>(
|
needsPersist: false,
|
||||||
persisted.get('session').accounts,
|
}))
|
||||||
)
|
const {accounts, currentAgent} = state
|
||||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
|
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
|
||||||
const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
|
const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
|
||||||
const currentAccountDid = React.useMemo(
|
const currentAccountDid = React.useMemo(
|
||||||
@@ -79,21 +75,23 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[accounts, currentAccountDid],
|
[accounts, currentAccountDid],
|
||||||
)
|
)
|
||||||
|
|
||||||
const persistNextUpdate = React.useCallback(
|
const switchAccountAndPersist = React.useCallback((newAgent: BskyAgent) => {
|
||||||
() => (isDirty.current = true),
|
const account = agentToSessionAccount(newAgent)
|
||||||
[],
|
setState(prev => {
|
||||||
)
|
let nextAccounts = prev.accounts
|
||||||
|
if (account) {
|
||||||
const upsertAndPersistAccount = React.useCallback(
|
nextAccounts = [
|
||||||
(account: SessionAccount) => {
|
account,
|
||||||
persistNextUpdate()
|
...prev.accounts.filter(a => a.did !== account.did),
|
||||||
setAccounts(accounts => [
|
]
|
||||||
account,
|
}
|
||||||
...accounts.filter(a => a.did !== account.did),
|
return {
|
||||||
])
|
accounts: nextAccounts,
|
||||||
},
|
currentAgent: newAgent,
|
||||||
[setAccounts, persistNextUpdate],
|
needsPersist: true,
|
||||||
)
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const clearCurrentAccount = React.useCallback(() => {
|
const clearCurrentAccount = React.useCallback(() => {
|
||||||
logger.warn(`session: clear current account`)
|
logger.warn(`session: clear current account`)
|
||||||
@@ -101,12 +99,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
// immediate clear this so any pending requests don't use it
|
// immediate clear this so any pending requests don't use it
|
||||||
currentAgent.setPersistSessionHandler(() => {})
|
currentAgent.setPersistSessionHandler(() => {})
|
||||||
|
|
||||||
persistNextUpdate()
|
|
||||||
|
|
||||||
const newAgent = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
|
const newAgent = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
|
||||||
configureModerationForGuest()
|
configureModerationForGuest()
|
||||||
setCurrentAgent(newAgent)
|
switchAccountAndPersist(newAgent)
|
||||||
}, [currentAgent, persistNextUpdate, setCurrentAgent])
|
}, [currentAgent, switchAccountAndPersist])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
/*
|
/*
|
||||||
@@ -161,14 +157,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
* The updated account object, derived from the updated session we just
|
* The updated account object, derived from the updated session we just
|
||||||
* received from this callback.
|
* received from this callback.
|
||||||
*/
|
*/
|
||||||
const refreshedAccount = agentToSessionAccount(currentAgent)
|
if (currentAgent.session) {
|
||||||
|
|
||||||
if (refreshedAccount) {
|
|
||||||
/*
|
/*
|
||||||
* If the session expired naturally, or it was otherwise successfully
|
* If the session expired naturally, or it was otherwise successfully
|
||||||
* created/updated, we want to update/persist the data.
|
* created/updated, we want to update/persist the data.
|
||||||
*/
|
*/
|
||||||
upsertAndPersistAccount(refreshedAccount)
|
switchAccountAndPersist(currentAgent)
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* This should never happen based on current `AtpAgent` handling, but
|
* This should never happen based on current `AtpAgent` handling, but
|
||||||
@@ -183,7 +177,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [currentAgent, clearCurrentAccount, upsertAndPersistAccount])
|
}, [currentAgent, clearCurrentAccount, switchAccountAndPersist])
|
||||||
|
|
||||||
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
|
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
|
||||||
async ({
|
async ({
|
||||||
@@ -199,34 +193,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
track('Try Create Account')
|
track('Try Create Account')
|
||||||
logEvent('account:create:begin', {})
|
logEvent('account:create:begin', {})
|
||||||
|
|
||||||
const {agent, account, fetchingGates} = await createAgentAndCreateAccount(
|
const {agent, fetchingGates} = await createAgentAndCreateAccount({
|
||||||
{
|
service,
|
||||||
service,
|
email,
|
||||||
email,
|
password,
|
||||||
password,
|
handle,
|
||||||
handle,
|
inviteCode,
|
||||||
inviteCode,
|
verificationPhone,
|
||||||
verificationPhone,
|
verificationCode,
|
||||||
verificationCode,
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
await fetchingGates
|
await fetchingGates
|
||||||
setCurrentAgent(agent)
|
switchAccountAndPersist(agent)
|
||||||
upsertAndPersistAccount(account)
|
|
||||||
|
|
||||||
logger.debug(`session: created account`, {}, logger.DebugContext.session)
|
logger.debug(`session: created account`, {}, logger.DebugContext.session)
|
||||||
track('Create Account')
|
track('Create Account')
|
||||||
logEvent('account:create:success', {})
|
logEvent('account:create:success', {})
|
||||||
},
|
},
|
||||||
[upsertAndPersistAccount],
|
[switchAccountAndPersist],
|
||||||
)
|
)
|
||||||
|
|
||||||
const login = React.useCallback<SessionApiContext['login']>(
|
const login = React.useCallback<SessionApiContext['login']>(
|
||||||
async ({service, identifier, password, authFactorToken}, logContext) => {
|
async ({service, identifier, password, authFactorToken}, logContext) => {
|
||||||
logger.debug(`session: login`, {}, logger.DebugContext.session)
|
logger.debug(`session: login`, {}, logger.DebugContext.session)
|
||||||
|
|
||||||
const {agent, account, fetchingGates} = await createAgentAndLogin({
|
const {agent, fetchingGates} = await createAgentAndLogin({
|
||||||
service,
|
service,
|
||||||
identifier,
|
identifier,
|
||||||
password,
|
password,
|
||||||
@@ -234,14 +225,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await fetchingGates
|
await fetchingGates
|
||||||
setCurrentAgent(agent)
|
switchAccountAndPersist(agent)
|
||||||
upsertAndPersistAccount(account)
|
|
||||||
|
|
||||||
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
|
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
|
||||||
track('Sign In', {resumedSession: false})
|
track('Sign In', {resumedSession: false})
|
||||||
logEvent('account:loggedIn', {logContext, withPassword: true})
|
logEvent('account:loggedIn', {logContext, withPassword: true})
|
||||||
},
|
},
|
||||||
[upsertAndPersistAccount],
|
[switchAccountAndPersist],
|
||||||
)
|
)
|
||||||
|
|
||||||
const logout = React.useCallback<SessionApiContext['logout']>(
|
const logout = React.useCallback<SessionApiContext['logout']>(
|
||||||
@@ -249,18 +239,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logger.debug(`session: logout`)
|
logger.debug(`session: logout`)
|
||||||
|
|
||||||
clearCurrentAccount()
|
clearCurrentAccount()
|
||||||
persistNextUpdate()
|
setState(prev => ({
|
||||||
setAccounts(accounts =>
|
accounts: prev.accounts.map(a => ({
|
||||||
accounts.map(a => ({
|
|
||||||
...a,
|
...a,
|
||||||
accessJwt: undefined,
|
accessJwt: undefined,
|
||||||
refreshJwt: undefined,
|
refreshJwt: undefined,
|
||||||
})),
|
})),
|
||||||
)
|
currentAgent: prev.currentAgent,
|
||||||
|
needsPersist: true,
|
||||||
|
}))
|
||||||
|
|
||||||
logEvent('account:loggedOut', {logContext})
|
logEvent('account:loggedOut', {logContext})
|
||||||
},
|
},
|
||||||
[clearCurrentAccount, persistNextUpdate, setAccounts],
|
[clearCurrentAccount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const initSession = React.useCallback<SessionApiContext['initSession']>(
|
const initSession = React.useCallback<SessionApiContext['initSession']>(
|
||||||
@@ -302,8 +293,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logger.DebugContext.session,
|
logger.DebugContext.session,
|
||||||
)
|
)
|
||||||
await networkRetry(1, () => newAgent.resumeSession(prevSession))
|
await networkRetry(1, () => newAgent.resumeSession(prevSession))
|
||||||
setCurrentAgent(newAgent)
|
switchAccountAndPersist(newAgent)
|
||||||
upsertAndPersistAccount(agentToSessionAccount(newAgent)!)
|
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* If the session is not expired, assume we can reuse it.
|
* If the session is not expired, assume we can reuse it.
|
||||||
@@ -314,11 +304,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logger.DebugContext.session,
|
logger.DebugContext.session,
|
||||||
)
|
)
|
||||||
newAgent.session = prevSession
|
newAgent.session = prevSession
|
||||||
setCurrentAgent(newAgent)
|
switchAccountAndPersist(newAgent)
|
||||||
upsertAndPersistAccount(account)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[upsertAndPersistAccount],
|
[switchAccountAndPersist],
|
||||||
)
|
)
|
||||||
|
|
||||||
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
|
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
|
||||||
@@ -338,10 +327,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
||||||
account => {
|
account => {
|
||||||
persistNextUpdate()
|
setState(prev => ({
|
||||||
setAccounts(accounts => accounts.filter(a => a.did !== account.did))
|
accounts: prev.accounts.filter(a => a.did !== account.did),
|
||||||
|
currentAgent: prev.currentAgent,
|
||||||
|
needsPersist: true,
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
[setAccounts, persistNextUpdate],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshSession = React.useCallback<
|
const refreshSession = React.useCallback<
|
||||||
@@ -358,16 +350,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
await newAgent.resumeSession(sessionAccountToAgentSession(selectedAccount)!)
|
await newAgent.resumeSession(sessionAccountToAgentSession(selectedAccount)!)
|
||||||
const refreshedAccount = agentToSessionAccount(newAgent)
|
const refreshedAccount = agentToSessionAccount(newAgent)
|
||||||
await configureModerationForAccount(newAgent, refreshedAccount!)
|
await configureModerationForAccount(newAgent, refreshedAccount!)
|
||||||
persistNextUpdate()
|
switchAccountAndPersist(newAgent)
|
||||||
upsertAndPersistAccount(refreshedAccount!)
|
}, [currentAccountDid, currentAgent, switchAccountAndPersist])
|
||||||
setCurrentAgent(newAgent)
|
|
||||||
}, [
|
|
||||||
currentAccountDid,
|
|
||||||
currentAgent,
|
|
||||||
setCurrentAgent,
|
|
||||||
persistNextUpdate,
|
|
||||||
upsertAndPersistAccount,
|
|
||||||
])
|
|
||||||
|
|
||||||
const updateCurrentAccount = React.useCallback(async () => {
|
const updateCurrentAccount = React.useCallback(async () => {
|
||||||
await refreshSession()
|
await refreshSession()
|
||||||
@@ -391,14 +375,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isDirty.current) {
|
if (state.needsPersist) {
|
||||||
isDirty.current = false
|
state.needsPersist = false
|
||||||
|
|
||||||
|
const currentAccountDid = state.currentAgent.session?.did
|
||||||
|
const currentAccount = state.accounts.find(
|
||||||
|
a => a.did === currentAccountDid,
|
||||||
|
)
|
||||||
persisted.write('session', {
|
persisted.write('session', {
|
||||||
accounts,
|
accounts: state.accounts,
|
||||||
currentAccount,
|
currentAccount: currentAccount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [accounts, currentAccount])
|
}, [state])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return persisted.onUpdate(async () => {
|
return persisted.onUpdate(async () => {
|
||||||
@@ -414,7 +403,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
* Accounts are already persisted on other side of broadcast, but we need
|
* Accounts are already persisted on other side of broadcast, but we need
|
||||||
* to update them in memory in this tab.
|
* to update them in memory in this tab.
|
||||||
*/
|
*/
|
||||||
setAccounts(persistedSession.accounts)
|
setState(prev => ({
|
||||||
|
accounts: persistedSession.accounts,
|
||||||
|
currentAgent: prev.currentAgent,
|
||||||
|
needsPersist: false, // We're syncing with another tab which already did that.
|
||||||
|
}))
|
||||||
|
|
||||||
const selectedAccount = persistedSession.accounts.find(
|
const selectedAccount = persistedSession.accounts.find(
|
||||||
a => a.did === persistedSession.currentAccount?.did,
|
a => a.did === persistedSession.currentAccount?.did,
|
||||||
@@ -450,7 +443,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
const newAgent = currentAgent.clone()
|
const newAgent = currentAgent.clone()
|
||||||
newAgent.session = sessionAccountToAgentSession(selectedAccount)
|
newAgent.session = sessionAccountToAgentSession(selectedAccount)
|
||||||
await configureModerationForAccount(newAgent, selectedAccount)
|
await configureModerationForAccount(newAgent, selectedAccount)
|
||||||
setCurrentAgent(newAgent)
|
setState(prev => ({
|
||||||
|
accounts: prev.accounts,
|
||||||
|
currentAgent: newAgent,
|
||||||
|
needsPersist: false, // We're syncing with another tab which already did that.
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
} else if (!selectedAccount && currentAccountDid) {
|
} else if (!selectedAccount && currentAccountDid) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -468,14 +465,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
clearCurrentAccount()
|
clearCurrentAccount()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [
|
}, [currentAccountDid, clearCurrentAccount, initSession, currentAgent])
|
||||||
currentAccountDid,
|
|
||||||
setAccounts,
|
|
||||||
clearCurrentAccount,
|
|
||||||
initSession,
|
|
||||||
currentAgent,
|
|
||||||
setCurrentAgent,
|
|
||||||
])
|
|
||||||
|
|
||||||
const stateContext = React.useMemo(
|
const stateContext = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ export async function createAgentAndLogin({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
agent,
|
agent,
|
||||||
account,
|
|
||||||
fetchingGates,
|
fetchingGates,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +184,6 @@ export async function createAgentAndCreateAccount({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
agent,
|
agent,
|
||||||
account,
|
|
||||||
fetchingGates,
|
fetchingGates,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user