Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 62 additions & 39 deletions src/state/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
createAgentAndResume,
sessionAccountToSession,
} from './agent'
import {getInitialState, reducer} from './reducer'
import {type Action, getInitialState, reducer, type State} from './reducer'

export {isSignupQueued} from './util'
import {addSessionDebugLog} from './logging'
Expand Down Expand Up @@ -46,29 +46,67 @@ const ApiContext = React.createContext<SessionApiContext>({
})
ApiContext.displayName = 'SessionApiContext'

export function Provider({children}: React.PropsWithChildren<{}>) {
const cancelPendingTask = useOneTaskAtATime()
const [state, dispatch] = React.useReducer(reducer, null, () => {
class SessionStore {
private state: State
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the case where there is no persisted session or no accounts? Could this result in an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The persisted abstraction fills that in with default values (i.e. we get [] here). This part is similar to how it worked before, I just moved it to a class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timing caveat is just that the initial read is async. We delay the component tree from mounting until then but reading in module scope would've read too early.

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(
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
emitSessionDropped()
}
dispatch({
store.dispatch({
type: 'received-agent-event',
agent,
refreshedAccount,
accountDid,
sessionEvent,
})
},
[],
[store],
)

const createAccount = React.useCallback<SessionApiContext['createAccount']>(
Expand All @@ -84,15 +122,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (signal.aborted) {
return
}
dispatch({
store.dispatch({
type: 'switched-to-account',
newAgent: agent,
newAccount: account,
})
logger.metric('account:create:success', metrics, {statsig: true})
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
},
[onAgentSessionChange, cancelPendingTask],
[store, onAgentSessionChange, cancelPendingTask],
)

const login = React.useCallback<SessionApiContext['login']>(
Expand All @@ -107,7 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (signal.aborted) {
return
}
dispatch({
store.dispatch({
type: 'switched-to-account',
newAgent: agent,
newAccount: account,
Expand All @@ -119,7 +157,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
addSessionDebugLog({type: 'method:end', method: 'login', account})
},
[onAgentSessionChange, cancelPendingTask],
[store, onAgentSessionChange, cancelPendingTask],
)

const logoutCurrentAccount = React.useCallback<
Expand All @@ -128,7 +166,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask()
dispatch({
store.dispatch({
type: 'logged-out-current-account',
})
logger.metric(
Expand All @@ -138,7 +176,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
addSessionDebugLog({type: 'method:end', method: 'logout'})
},
[cancelPendingTask],
[store, cancelPendingTask],
)

const logoutEveryAccount = React.useCallback<
Expand All @@ -147,7 +185,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask()
dispatch({
store.dispatch({
type: 'logged-out-every-account',
})
logger.metric(
Expand All @@ -157,7 +195,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
addSessionDebugLog({type: 'method:end', method: 'logout'})
},
[cancelPendingTask],
[store, cancelPendingTask],
)

const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
Expand All @@ -176,14 +214,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (signal.aborted) {
return
}
dispatch({
store.dispatch({
type: 'switched-to-account',
newAgent: agent,
newAccount: account,
})
addSessionDebugLog({type: 'method:end', method: 'resumeSession', account})
},
[onAgentSessionChange, cancelPendingTask],
[store, onAgentSessionChange, cancelPendingTask],
)

const partialRefreshSession = React.useCallback<
Expand All @@ -193,15 +231,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const signal = cancelPendingTask()
const {data} = await agent.com.atproto.server.getSession()
if (signal.aborted) return
dispatch({
store.dispatch({
type: 'partial-refresh-session',
accountDid: agent.session!.did,
patch: {
emailConfirmed: data.emailConfirmed,
emailAuthFactor: data.emailAuthFactor,
},
})
}, [state, cancelPendingTask])
}, [store, state, cancelPendingTask])

const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => {
Expand All @@ -211,34 +249,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
account,
})
cancelPendingTask()
dispatch({
store.dispatch({
type: 'removed-account',
accountDid: account.did,
})
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(() => {
return persisted.onUpdate('session', nextSession => {
const synced = nextSession
addSessionDebugLog({type: 'persisted:receive', data: synced})
dispatch({
store.dispatch({
type: 'synced-accounts',
syncedAccounts: synced.accounts,
syncedCurrentDid: synced.currentAccount?.did,
Expand All @@ -262,7 +285,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}
})
}, [state, resumeSession])
}, [store, state, resumeSession])

const stateContext = React.useMemo(
() => ({
Expand Down
Loading