@@ -14,7 +14,7 @@ import {
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,29 +46,67 @@ const ApiContext = React.createContext<SessionApiContext>({
46
46
} )
47
47
ApiContext . displayName = 'SessionApiContext'
48
48
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.
52
55
const initialState = getInitialState ( persisted . get ( 'session' ) . accounts )
53
56
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 )
56
94
57
95
const onAgentSessionChange = React . useCallback (
58
96
( agent : BskyAgent , accountDid : string , sessionEvent : AtpSessionEvent ) => {
59
97
const refreshedAccount = agentToSessionAccount ( agent ) // Mutable, so snapshot it right away.
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,
67
105
accountDid,
68
106
sessionEvent,
69
107
} )
70
108
} ,
71
- [ ] ,
109
+ [ store ] ,
72
110
)
73
111
74
112
const createAccount = React . useCallback < SessionApiContext [ 'createAccount' ] > (
@@ -84,15 +122,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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 ,
91
129
} )
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,7 +145,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,7 +157,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,7 +166,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,7 +176,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,7 +185,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,7 +195,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,14 +214,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,15 +231,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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 : {
200
238
emailConfirmed : data . emailConfirmed ,
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,34 +249,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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,7 +285,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
262
285
}
263
286
}
264
287
} )
265
- } , [ state , resumeSession ] )
288
+ } , [ store , state , resumeSession ] )
266
289
267
290
const stateContext = React . useMemo (
268
291
( ) => ( {
0 commit comments