@@ -148,6 +148,70 @@ it.live("token refresh persists the new token", () =>
148148 } ) ,
149149)
150150
151+ it . live ( "concurrent config and token requests coalesce token refresh" , ( ) =>
152+ Effect . gen ( function * ( ) {
153+ const id = AccountID . make ( "user-1" )
154+
155+ yield * AccountRepo . use ( ( r ) =>
156+ r . persistAccount ( {
157+ id,
158+ email : "user@example.com" ,
159+ url : "https://one.example.com" ,
160+ accessToken : AccessToken . make ( "at_old" ) ,
161+ refreshToken : RefreshToken . make ( "rt_old" ) ,
162+ expiry : Date . now ( ) - 1_000 ,
163+ orgID : Option . some ( OrgID . make ( "org-9" ) ) ,
164+ } ) ,
165+ )
166+
167+ let refreshCalls = 0
168+ const client = HttpClient . make ( ( req ) =>
169+ Effect . promise ( async ( ) => {
170+ if ( req . url === "https://one.example.com/auth/device/token" ) {
171+ refreshCalls += 1
172+
173+ if ( refreshCalls === 1 ) {
174+ await new Promise ( ( resolve ) => setTimeout ( resolve , 25 ) )
175+ return json ( req , {
176+ access_token : "at_new" ,
177+ refresh_token : "rt_new" ,
178+ expires_in : 60 ,
179+ } )
180+ }
181+
182+ return json (
183+ req ,
184+ {
185+ error : "invalid_grant" ,
186+ error_description : "refresh token already used" ,
187+ } ,
188+ 400 ,
189+ )
190+ }
191+
192+ if ( req . url === "https://one.example.com/api/config" ) {
193+ return json ( req , { config : { theme : "light" , seats : 5 } } )
194+ }
195+
196+ return json ( req , { } , 404 )
197+ } ) ,
198+ )
199+
200+ const [ cfg , token ] = yield * Account . Service . use ( ( s ) =>
201+ Effect . all ( [ s . config ( id , OrgID . make ( "org-9" ) ) , s . token ( id ) ] , { concurrency : 2 } ) ,
202+ ) . pipe ( Effect . provide ( live ( client ) ) )
203+
204+ expect ( Option . getOrThrow ( cfg ) ) . toEqual ( { theme : "light" , seats : 5 } )
205+ expect ( String ( Option . getOrThrow ( token ) ) ) . toBe ( "at_new" )
206+ expect ( refreshCalls ) . toBe ( 1 )
207+
208+ const row = yield * AccountRepo . use ( ( r ) => r . getRow ( id ) )
209+ const value = Option . getOrThrow ( row )
210+ expect ( value . access_token ) . toBe ( AccessToken . make ( "at_new" ) )
211+ expect ( value . refresh_token ) . toBe ( RefreshToken . make ( "rt_new" ) )
212+ } ) ,
213+ )
214+
151215it . live ( "config sends the selected org header" , ( ) =>
152216 Effect . gen ( function * ( ) {
153217 const id = AccountID . make ( "user-1" )
0 commit comments