+62
-39
src/state/session/index.tsx
+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
() => ({