@@ -18,6 +18,7 @@ final class SessionManager {
1818 private var refreshRetryCount = 0
1919 private static let maxRefreshRetries = 3
2020 private static let retryDelays : [ TimeInterval ] = [ 3 , 10 , 30 ]
21+ private var refreshInFlight : Task < TokenPair ? , Never > ?
2122 private let onExpired : ( ) -> Void
2223
2324 init ( api: AuthonAPI , onRefreshed: @escaping ( TokenPair , AuthonUser ) -> Void , onExpired: @escaping ( ) -> Void ) {
@@ -115,27 +116,38 @@ final class SessionManager {
115116 }
116117
117118 func refresh( ) async -> TokenPair ? {
119+ // Single-flight: if refresh is already in progress, wait for that result
120+ if let existing = refreshInFlight {
121+ return await existing. value
122+ }
118123 guard let refreshToken = tokens? . refreshToken, !refreshToken. isEmpty else { return nil }
119- do {
120- struct RefreshBody : Encodable {
121- let refreshToken : String
124+ let task = Task < TokenPair ? , Never > { [ weak self] in
125+ guard let self else { return nil }
126+ do {
127+ struct RefreshBody : Encodable {
128+ let refreshToken : String
129+ }
130+ let response : ApiAuthResponse = try await self . api. request (
131+ " POST " ,
132+ " /v1/auth/token/refresh " ,
133+ body: RefreshBody ( refreshToken: refreshToken)
134+ )
135+ let newPair = TokenPair (
136+ accessToken: response. accessToken,
137+ refreshToken: response. refreshToken,
138+ expiresAt: Date ( ) . timeIntervalSince1970 * 1000 + Double( response. expiresIn) * 1000
139+ )
140+ self . tokens = newPair
141+ self . saveToKeychain ( newPair)
142+ return newPair
143+ } catch {
144+ return nil
122145 }
123- let response : ApiAuthResponse = try await api. request (
124- " POST " ,
125- " /v1/auth/token/refresh " ,
126- body: RefreshBody ( refreshToken: refreshToken)
127- )
128- let newPair = TokenPair (
129- accessToken: response. accessToken,
130- refreshToken: response. refreshToken,
131- expiresAt: Date ( ) . timeIntervalSince1970 * 1000 + Double( response. expiresIn) * 1000
132- )
133- tokens = newPair
134- saveToKeychain ( newPair)
135- return newPair
136- } catch {
137- return nil
138146 }
147+ refreshInFlight = task
148+ let result = await task. value
149+ refreshInFlight = nil
150+ return result
139151 }
140152
141153 // MARK: - Auto-Refresh
@@ -166,6 +178,9 @@ final class SessionManager {
166178 }
167179
168180 private func performRefresh( ) {
181+ // Skip if refresh is already in flight
182+ if refreshInFlight != nil { return }
183+
169184 guard let refreshToken = tokens? . refreshToken else {
170185 onExpired ( )
171186 return
0 commit comments