Skip to content

Commit 8853562

Browse files
authored
[Fix Logouts] Persist accounts synchronously (#9109)
* Make persisting synchronous * Initialize later so persisted is filled
1 parent b68f800 commit 8853562

File tree

1 file changed

+62
-39
lines changed

1 file changed

+62
-39
lines changed

src/state/session/index.tsx

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
createAgentAndResume,
1515
sessionAccountToSession,
1616
} from './agent'
17-
import {getInitialState, reducer} from './reducer'
17+
import {type Action, getInitialState, reducer, type State} from './reducer'
1818

1919
export {isSignupQueued} from './util'
2020
import {addSessionDebugLog} from './logging'
@@ -46,29 +46,67 @@ const ApiContext = React.createContext<SessionApiContext>({
4646
})
4747
ApiContext.displayName = 'SessionApiContext'
4848

49-
export function Provider({children}: React.PropsWithChildren<{}>) {
50-
const cancelPendingTask = useOneTaskAtATime()
51-
const [state, dispatch] = React.useReducer(reducer, null, () => {
49+
class SessionStore {
50+
private state: State
51+
private listeners = new Set<() => void>()
52+
53+
constructor() {
54+
// Careful: By the time this runs, `persisted` needs to already be filled.
5255
const initialState = getInitialState(persisted.get('session').accounts)
5356
addSessionDebugLog({type: 'reducer:init', state: initialState})
54-
return initialState
55-
})
57+
this.state = initialState
58+
}
59+
60+
getState = (): State => {
61+
return this.state
62+
}
63+
64+
subscribe = (listener: () => void) => {
65+
this.listeners.add(listener)
66+
return () => {
67+
this.listeners.delete(listener)
68+
}
69+
}
70+
71+
dispatch = (action: Action) => {
72+
const nextState = reducer(this.state, action)
73+
this.state = nextState
74+
// Persist synchronously without waiting for the React render cycle.
75+
if (nextState.needsPersist) {
76+
nextState.needsPersist = false
77+
const persistedData = {
78+
accounts: nextState.accounts,
79+
currentAccount: nextState.accounts.find(
80+
a => a.did === nextState.currentAgentState.did,
81+
),
82+
}
83+
addSessionDebugLog({type: 'persisted:broadcast', data: persistedData})
84+
persisted.write('session', persistedData)
85+
}
86+
this.listeners.forEach(listener => listener())
87+
}
88+
}
89+
90+
export function Provider({children}: React.PropsWithChildren<{}>) {
91+
const cancelPendingTask = useOneTaskAtATime()
92+
const [store] = React.useState(() => new SessionStore())
93+
const state = React.useSyncExternalStore(store.subscribe, store.getState)
5694

5795
const onAgentSessionChange = React.useCallback(
5896
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
5997
const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.
6098
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
6199
emitSessionDropped()
62100
}
63-
dispatch({
101+
store.dispatch({
64102
type: 'received-agent-event',
65103
agent,
66104
refreshedAccount,
67105
accountDid,
68106
sessionEvent,
69107
})
70108
},
71-
[],
109+
[store],
72110
)
73111

74112
const createAccount = React.useCallback<SessionApiContext['createAccount']>(
@@ -84,15 +122,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
84122
if (signal.aborted) {
85123
return
86124
}
87-
dispatch({
125+
store.dispatch({
88126
type: 'switched-to-account',
89127
newAgent: agent,
90128
newAccount: account,
91129
})
92130
logger.metric('account:create:success', metrics, {statsig: true})
93131
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
94132
},
95-
[onAgentSessionChange, cancelPendingTask],
133+
[store, onAgentSessionChange, cancelPendingTask],
96134
)
97135

98136
const login = React.useCallback<SessionApiContext['login']>(
@@ -107,7 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
107145
if (signal.aborted) {
108146
return
109147
}
110-
dispatch({
148+
store.dispatch({
111149
type: 'switched-to-account',
112150
newAgent: agent,
113151
newAccount: account,
@@ -119,7 +157,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
119157
)
120158
addSessionDebugLog({type: 'method:end', method: 'login', account})
121159
},
122-
[onAgentSessionChange, cancelPendingTask],
160+
[store, onAgentSessionChange, cancelPendingTask],
123161
)
124162

125163
const logoutCurrentAccount = React.useCallback<
@@ -128,7 +166,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
128166
logContext => {
129167
addSessionDebugLog({type: 'method:start', method: 'logout'})
130168
cancelPendingTask()
131-
dispatch({
169+
store.dispatch({
132170
type: 'logged-out-current-account',
133171
})
134172
logger.metric(
@@ -138,7 +176,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
138176
)
139177
addSessionDebugLog({type: 'method:end', method: 'logout'})
140178
},
141-
[cancelPendingTask],
179+
[store, cancelPendingTask],
142180
)
143181

144182
const logoutEveryAccount = React.useCallback<
@@ -147,7 +185,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
147185
logContext => {
148186
addSessionDebugLog({type: 'method:start', method: 'logout'})
149187
cancelPendingTask()
150-
dispatch({
188+
store.dispatch({
151189
type: 'logged-out-every-account',
152190
})
153191
logger.metric(
@@ -157,7 +195,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
157195
)
158196
addSessionDebugLog({type: 'method:end', method: 'logout'})
159197
},
160-
[cancelPendingTask],
198+
[store, cancelPendingTask],
161199
)
162200

163201
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
@@ -176,14 +214,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
176214
if (signal.aborted) {
177215
return
178216
}
179-
dispatch({
217+
store.dispatch({
180218
type: 'switched-to-account',
181219
newAgent: agent,
182220
newAccount: account,
183221
})
184222
addSessionDebugLog({type: 'method:end', method: 'resumeSession', account})
185223
},
186-
[onAgentSessionChange, cancelPendingTask],
224+
[store, onAgentSessionChange, cancelPendingTask],
187225
)
188226

189227
const partialRefreshSession = React.useCallback<
@@ -193,15 +231,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
193231
const signal = cancelPendingTask()
194232
const {data} = await agent.com.atproto.server.getSession()
195233
if (signal.aborted) return
196-
dispatch({
234+
store.dispatch({
197235
type: 'partial-refresh-session',
198236
accountDid: agent.session!.did,
199237
patch: {
200238
emailConfirmed: data.emailConfirmed,
201239
emailAuthFactor: data.emailAuthFactor,
202240
},
203241
})
204-
}, [state, cancelPendingTask])
242+
}, [store, state, cancelPendingTask])
205243

206244
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
207245
account => {
@@ -211,34 +249,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
211249
account,
212250
})
213251
cancelPendingTask()
214-
dispatch({
252+
store.dispatch({
215253
type: 'removed-account',
216254
accountDid: account.did,
217255
})
218256
addSessionDebugLog({type: 'method:end', method: 'removeAccount', account})
219257
},
220-
[cancelPendingTask],
258+
[store, cancelPendingTask],
221259
)
222-
223-
React.useEffect(() => {
224-
if (state.needsPersist) {
225-
state.needsPersist = false
226-
const persistedData = {
227-
accounts: state.accounts,
228-
currentAccount: state.accounts.find(
229-
a => a.did === state.currentAgentState.did,
230-
),
231-
}
232-
addSessionDebugLog({type: 'persisted:broadcast', data: persistedData})
233-
persisted.write('session', persistedData)
234-
}
235-
}, [state])
236-
237260
React.useEffect(() => {
238261
return persisted.onUpdate('session', nextSession => {
239262
const synced = nextSession
240263
addSessionDebugLog({type: 'persisted:receive', data: synced})
241-
dispatch({
264+
store.dispatch({
242265
type: 'synced-accounts',
243266
syncedAccounts: synced.accounts,
244267
syncedCurrentDid: synced.currentAccount?.did,
@@ -262,7 +285,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
262285
}
263286
}
264287
})
265-
}, [state, resumeSession])
288+
}, [store, state, resumeSession])
266289

267290
const stateContext = React.useMemo(
268291
() => ({

0 commit comments

Comments
 (0)