deer social fork for personal usage. but you might see a use idk. github mirror

[Fix Logouts] Persist accounts synchronously (#9109)

* Make persisting synchronous

* Initialize later so persisted is filled

authored by danabra.mov and committed by GitHub 88535625 b68f8005

Changed files
+62 -39
src
state
session
+62 -39
src/state/session/index.tsx
··· 14 14 createAgentAndResume, 15 15 sessionAccountToSession, 16 16 } from './agent' 17 - import {getInitialState, reducer} from './reducer' 17 + import {type Action, getInitialState, reducer, type State} from './reducer' 18 18 19 19 export {isSignupQueued} from './util' 20 20 import {addSessionDebugLog} from './logging' ··· 46 46 }) 47 47 ApiContext.displayName = 'SessionApiContext' 48 48 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. 55 + const initialState = getInitialState(persisted.get('session').accounts) 56 + addSessionDebugLog({type: 'reducer:init', state: initialState}) 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 + 49 90 export function Provider({children}: React.PropsWithChildren<{}>) { 50 91 const cancelPendingTask = useOneTaskAtATime() 51 - const [state, dispatch] = React.useReducer(reducer, null, () => { 52 - const initialState = getInitialState(persisted.get('session').accounts) 53 - addSessionDebugLog({type: 'reducer:init', state: initialState}) 54 - return initialState 55 - }) 92 + const [store] = React.useState(() => new SessionStore()) 93 + const state = React.useSyncExternalStore(store.subscribe, store.getState) 56 94 57 95 const onAgentSessionChange = React.useCallback( 58 96 (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { ··· 60 98 if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { 61 99 emitSessionDropped() 62 100 } 63 - dispatch({ 101 + store.dispatch({ 64 102 type: 'received-agent-event', 65 103 agent, 66 104 refreshedAccount, ··· 68 106 sessionEvent, 69 107 }) 70 108 }, 71 - [], 109 + [store], 72 110 ) 73 111 74 112 const createAccount = React.useCallback<SessionApiContext['createAccount']>( ··· 84 122 if (signal.aborted) { 85 123 return 86 124 } 87 - dispatch({ 125 + store.dispatch({ 88 126 type: 'switched-to-account', 89 127 newAgent: agent, 90 128 newAccount: account, ··· 92 130 logger.metric('account:create:success', metrics, {statsig: true}) 93 131 addSessionDebugLog({type: 'method:end', method: 'createAccount', account}) 94 132 }, 95 - [onAgentSessionChange, cancelPendingTask], 133 + [store, onAgentSessionChange, cancelPendingTask], 96 134 ) 97 135 98 136 const login = React.useCallback<SessionApiContext['login']>( ··· 107 145 if (signal.aborted) { 108 146 return 109 147 } 110 - dispatch({ 148 + store.dispatch({ 111 149 type: 'switched-to-account', 112 150 newAgent: agent, 113 151 newAccount: account, ··· 119 157 ) 120 158 addSessionDebugLog({type: 'method:end', method: 'login', account}) 121 159 }, 122 - [onAgentSessionChange, cancelPendingTask], 160 + [store, onAgentSessionChange, cancelPendingTask], 123 161 ) 124 162 125 163 const logoutCurrentAccount = React.useCallback< ··· 128 166 logContext => { 129 167 addSessionDebugLog({type: 'method:start', method: 'logout'}) 130 168 cancelPendingTask() 131 - dispatch({ 169 + store.dispatch({ 132 170 type: 'logged-out-current-account', 133 171 }) 134 172 logger.metric( ··· 138 176 ) 139 177 addSessionDebugLog({type: 'method:end', method: 'logout'}) 140 178 }, 141 - [cancelPendingTask], 179 + [store, cancelPendingTask], 142 180 ) 143 181 144 182 const logoutEveryAccount = React.useCallback< ··· 147 185 logContext => { 148 186 addSessionDebugLog({type: 'method:start', method: 'logout'}) 149 187 cancelPendingTask() 150 - dispatch({ 188 + store.dispatch({ 151 189 type: 'logged-out-every-account', 152 190 }) 153 191 logger.metric( ··· 157 195 ) 158 196 addSessionDebugLog({type: 'method:end', method: 'logout'}) 159 197 }, 160 - [cancelPendingTask], 198 + [store, cancelPendingTask], 161 199 ) 162 200 163 201 const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( ··· 176 214 if (signal.aborted) { 177 215 return 178 216 } 179 - dispatch({ 217 + store.dispatch({ 180 218 type: 'switched-to-account', 181 219 newAgent: agent, 182 220 newAccount: account, 183 221 }) 184 222 addSessionDebugLog({type: 'method:end', method: 'resumeSession', account}) 185 223 }, 186 - [onAgentSessionChange, cancelPendingTask], 224 + [store, onAgentSessionChange, cancelPendingTask], 187 225 ) 188 226 189 227 const partialRefreshSession = React.useCallback< ··· 193 231 const signal = cancelPendingTask() 194 232 const {data} = await agent.com.atproto.server.getSession() 195 233 if (signal.aborted) return 196 - dispatch({ 234 + store.dispatch({ 197 235 type: 'partial-refresh-session', 198 236 accountDid: agent.session!.did, 199 237 patch: { ··· 201 239 emailAuthFactor: data.emailAuthFactor, 202 240 }, 203 241 }) 204 - }, [state, cancelPendingTask]) 242 + }, [store, state, cancelPendingTask]) 205 243 206 244 const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( 207 245 account => { ··· 211 249 account, 212 250 }) 213 251 cancelPendingTask() 214 - dispatch({ 252 + store.dispatch({ 215 253 type: 'removed-account', 216 254 accountDid: account.did, 217 255 }) 218 256 addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) 219 257 }, 220 - [cancelPendingTask], 258 + [store, cancelPendingTask], 221 259 ) 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 - 237 260 React.useEffect(() => { 238 261 return persisted.onUpdate('session', nextSession => { 239 262 const synced = nextSession 240 263 addSessionDebugLog({type: 'persisted:receive', data: synced}) 241 - dispatch({ 264 + store.dispatch({ 242 265 type: 'synced-accounts', 243 266 syncedAccounts: synced.accounts, 244 267 syncedCurrentDid: synced.currentAccount?.did, ··· 262 285 } 263 286 } 264 287 }) 265 - }, [state, resumeSession]) 288 + }, [store, state, resumeSession]) 266 289 267 290 const stateContext = React.useMemo( 268 291 () => ({