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