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