Files
hyungi_document_server/clients/ds-app/Sources/DSKit/Auth/TokenProvider.swift
T

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
}
}