f512d94c74
git-subtree-dir: clients/ds-app git-subtree-mainline:a24e3e6f22git-subtree-split:5206cf3b0c
44 lines
1.5 KiB
Swift
44 lines
1.5 KiB
Swift
import Foundation
|
|
|
|
/// Concurrency-safe access-token holder with SINGLE-FLIGHT refresh: concurrent 401s must not each
|
|
/// fire `/auth/refresh`. The coalescing is via a stored `Task` handle (not a bool) — entrants await
|
|
/// the same in-flight task; it's cleared (success OR failure) when complete. Clearing too early lets a
|
|
/// follower re-refresh; never clearing means the next expiry won't refresh.
|
|
public actor TokenProvider {
|
|
private var cached: String?
|
|
private let persistence: TokenPersistence
|
|
private let refresh: @Sendable () async throws -> String
|
|
private var inFlightRefresh: Task<String, Error>?
|
|
|
|
public init(persistence: TokenPersistence, refresh: @escaping @Sendable () async throws -> String) {
|
|
self.persistence = persistence
|
|
self.refresh = refresh
|
|
self.cached = persistence.read()
|
|
}
|
|
|
|
public func current() -> String? { cached ?? persistence.read() }
|
|
|
|
public func set(_ token: String) {
|
|
cached = token
|
|
try? persistence.save(token)
|
|
}
|
|
|
|
public func clear() {
|
|
cached = nil
|
|
try? persistence.delete()
|
|
}
|
|
|
|
public func refreshOnce() async throws -> String {
|
|
if let task = inFlightRefresh {
|
|
return try await task.value
|
|
}
|
|
let task = Task { try await self.refresh() }
|
|
inFlightRefresh = task
|
|
defer { inFlightRefresh = nil }
|
|
let token = try await task.value
|
|
cached = token
|
|
try? persistence.save(token)
|
|
return token
|
|
}
|
|
}
|