200 lines
6.0 KiB
Swift
200 lines
6.0 KiB
Swift
import Foundation
|
|
|
|
@MainActor
|
|
final class ProviderRefreshCoordinator<Key: Hashable> {
|
|
enum WaitResult: Equatable {
|
|
case completed
|
|
case retryRequired
|
|
case cancelled
|
|
}
|
|
|
|
struct Request {
|
|
let generation: UInt64
|
|
let state: ProviderRefreshTaskState
|
|
let predecessorStates: [ProviderRefreshTaskState]
|
|
}
|
|
|
|
private var states: [Key: [ProviderRefreshTaskState]] = [:]
|
|
private var latestGenerations: [Key: UInt64] = [:]
|
|
private var activeCounts: [Key: Int] = [:]
|
|
private var nextGeneration: UInt64 = 0
|
|
private var nextWaiterID: UInt64 = 0
|
|
|
|
func coalescingState(for key: Key) -> ProviderRefreshTaskState? {
|
|
guard let latestGeneration = self.latestGenerations[key] else { return nil }
|
|
return self.states[key]?.last { state in
|
|
state.generation == latestGeneration && !state.isCompleted
|
|
}
|
|
}
|
|
|
|
func beginReplacingRequest(for key: Key) -> Request {
|
|
self.nextGeneration &+= 1
|
|
let generation = self.nextGeneration
|
|
let predecessorStates = self.states[key] ?? []
|
|
for predecessorState in predecessorStates {
|
|
predecessorState.cancelTask()
|
|
}
|
|
self.latestGenerations[key] = generation
|
|
let state = ProviderRefreshTaskState(generation: generation)
|
|
self.states[key, default: []].append(state)
|
|
return Request(
|
|
generation: generation,
|
|
state: state,
|
|
predecessorStates: predecessorStates)
|
|
}
|
|
|
|
/// Invalidates in-flight work without creating a replacement request. Existing states stay
|
|
/// registered until their tasks and waiters drain, but their generations can no longer publish.
|
|
func invalidateRequests(for key: Key) {
|
|
self.nextGeneration &+= 1
|
|
self.latestGenerations[key] = self.nextGeneration
|
|
for state in self.states[key] ?? [] {
|
|
state.cancelTask()
|
|
}
|
|
}
|
|
|
|
func wait(for key: Key, state: ProviderRefreshTaskState) async -> WaitResult {
|
|
self.nextWaiterID &+= 1
|
|
let waiterID = self.nextWaiterID
|
|
guard let task = state.addWaiter(waiterID) else { return .completed }
|
|
await withTaskCancellationHandler {
|
|
await task.value
|
|
} onCancel: {
|
|
state.cancelWaiter(waiterID)
|
|
}
|
|
state.finishWaiter(waiterID)
|
|
let result: WaitResult = if Task.isCancelled {
|
|
.cancelled
|
|
} else if state.shouldRetry {
|
|
.retryRequired
|
|
} else {
|
|
.completed
|
|
}
|
|
if state.canRemove {
|
|
self.scheduleRemoval(for: key, state: state)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func complete(_ state: ProviderRefreshTaskState, for key: Key, retryRequired: Bool) {
|
|
state.markCompleted(retryRequired: retryRequired)
|
|
self.scheduleRemoval(for: key, state: state)
|
|
}
|
|
|
|
func remove(_ state: ProviderRefreshTaskState, for key: Key) {
|
|
guard var keyStates = self.states[key] else { return }
|
|
keyStates.removeAll { $0 === state }
|
|
if keyStates.isEmpty {
|
|
self.states.removeValue(forKey: key)
|
|
} else {
|
|
self.states[key] = keyStates
|
|
}
|
|
}
|
|
|
|
func isCurrent(_ generation: UInt64, for key: Key) -> Bool {
|
|
self.latestGenerations[key] == generation
|
|
}
|
|
|
|
@discardableResult
|
|
func beginActivity(for key: Key) -> Bool {
|
|
self.activeCounts[key, default: 0] += 1
|
|
return self.activeCounts[key] == 1
|
|
}
|
|
|
|
@discardableResult
|
|
func endActivity(for key: Key) -> Bool {
|
|
let remaining = max(0, self.activeCounts[key, default: 1] - 1)
|
|
if remaining == 0 {
|
|
self.activeCounts.removeValue(forKey: key)
|
|
return true
|
|
}
|
|
self.activeCounts[key] = remaining
|
|
return false
|
|
}
|
|
|
|
private func scheduleRemoval(for key: Key, state: ProviderRefreshTaskState) {
|
|
Task { @MainActor [weak self] in
|
|
await Task.yield()
|
|
guard let self,
|
|
self.states[key]?.contains(where: { $0 === state }) == true,
|
|
state.canRemove
|
|
else {
|
|
return
|
|
}
|
|
self.remove(state, for: key)
|
|
}
|
|
}
|
|
}
|
|
|
|
final class ProviderRefreshTaskState: @unchecked Sendable {
|
|
let generation: UInt64
|
|
|
|
private let lock = NSLock()
|
|
private var task: Task<Void, Never>?
|
|
private var waiterIDs: Set<UInt64> = []
|
|
private var completed = false
|
|
private var retryRequired = false
|
|
|
|
init(generation: UInt64) {
|
|
self.generation = generation
|
|
}
|
|
|
|
func install(task: Task<Void, Never>) {
|
|
self.lock.withLock {
|
|
self.task = task
|
|
}
|
|
}
|
|
|
|
func addWaiter(_ waiterID: UInt64) -> Task<Void, Never>? {
|
|
self.lock.withLock {
|
|
self.waiterIDs.insert(waiterID)
|
|
return self.task
|
|
}
|
|
}
|
|
|
|
func cancelWaiter(_ waiterID: UInt64) {
|
|
let taskToCancel = self.lock.withLock {
|
|
guard self.waiterIDs.remove(waiterID) != nil else { return nil as Task<Void, Never>? }
|
|
return self.waiterIDs.isEmpty && !self.completed ? self.task : nil
|
|
}
|
|
taskToCancel?.cancel()
|
|
}
|
|
|
|
func finishWaiter(_ waiterID: UInt64) {
|
|
_ = self.lock.withLock {
|
|
self.waiterIDs.remove(waiterID)
|
|
}
|
|
}
|
|
|
|
func markCompleted(retryRequired: Bool) {
|
|
self.lock.withLock {
|
|
self.completed = true
|
|
self.retryRequired = retryRequired
|
|
}
|
|
}
|
|
|
|
func cancelTask() {
|
|
let task = self.lock.withLock {
|
|
self.completed ? nil : self.task
|
|
}
|
|
task?.cancel()
|
|
}
|
|
|
|
func waitForTaskCompletion() async {
|
|
let task = self.lock.withLock { self.task }
|
|
await task?.value
|
|
}
|
|
|
|
fileprivate var shouldRetry: Bool {
|
|
self.lock.withLock { self.retryRequired }
|
|
}
|
|
|
|
fileprivate var isCompleted: Bool {
|
|
self.lock.withLock { self.completed }
|
|
}
|
|
|
|
var canRemove: Bool {
|
|
self.lock.withLock { self.completed && self.waiterIDs.isEmpty }
|
|
}
|
|
}
|