Last active
March 10, 2024 21:44
-
-
Save thomsmed/1f465b4bb0bdb473ff24ab7171dfaa54 to your computer and use it in GitHub Desktop.
A concurrent and thread safe resource cache that will make the first resource requesting Task fetch the resource asynchronously while being suspended. All subsequent Tasks requesting the resource will also be suspended, and a Continuation will be created and stored for each Task. Continuations and Tasks will then be resumed once the initial Task…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContinuationCreatingResourceCache.swift | |
// | |
import Foundation | |
/// A thread/concurrency context safe storage for Continuations using [NSLock](https://developer.apple.com/documentation/foundation/nslock). | |
/// Heavily inspired by this WWDC 2023 video: [Beyond the basics of structured concurrency](https://developer.apple.com/videos/play/wwdc2023/10170/). | |
/// | |
/// NOTE: Be careful to always resume any suspended Continuations to prevent Continuation / Task leaks. | |
/// Highly recommend to check out this WWDC 2022 video: [Visualize and optimize Swift concurrency](https://developer.apple.com/videos/play/wwdc2022/110350/). | |
final class WaitingContinuationsLocker: @unchecked Sendable { | |
private var protectedWaitingContinuations: [UUID: CheckedContinuation<Data?, Never>] = [:] | |
private let lock = NSLock() | |
var count: Int { | |
lock.withLock { protectedWaitingContinuations.count } | |
} | |
func set(_ waitingContinuation: CheckedContinuation<Data?, Never>, forId id: UUID) { | |
_ = lock.withLock { protectedWaitingContinuations.updateValue(waitingContinuation, forKey: id) } | |
} | |
func popFirst() -> (UUID, CheckedContinuation<Data?, Never>)? { | |
lock.withLock { protectedWaitingContinuations.popFirst() } | |
} | |
func remove(forId id: UUID) -> CheckedContinuation<Data?, Never>? { | |
lock.withLock { protectedWaitingContinuations.removeValue(forKey: id) } | |
} | |
} | |
/// A concurrent and thread safe resource cache that will make the first resource requesting Task fetch the resource asynchronously while being suspended. | |
/// All subsequent Tasks requesting the resource will also be suspended, and a Continuation will be created and stored for each Task. | |
/// Continuations and Tasks will then be resumed once the initial Task has finished fetching the resource. | |
final actor ContinuationCreatingResourceCache { | |
private enum CachedResourceState { | |
case none | |
case fetching | |
case value(Data?) | |
} | |
private let urlSession: URLSession | |
private let waitingContinuationsLocker = WaitingContinuationsLocker() | |
private var cachedResourceState: CachedResourceState = .none | |
init(urlSession: URLSession = .shared) { | |
self.urlSession = urlSession | |
} | |
// MARK: - Private | |
private func fetchResource() async -> Data? { | |
let url = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/132.png")! | |
let request = URLRequest(url: url) | |
do { | |
let (data, _) = try await urlSession.data(for: request) | |
return data | |
} catch { | |
// Ignoring proper Error handling for simplicity. | |
print("Failed to fetch resource:", error) | |
return nil | |
} | |
} | |
// MARK: - Public | |
var resource: Data? { | |
get async { | |
switch cachedResourceState { | |
case .none: | |
cachedResourceState = .fetching | |
let resource = await fetchResource() | |
cachedResourceState = .value(resource) | |
while let (_, waitingContinuation) = waitingContinuationsLocker.popFirst() { | |
// Resume suspended Continuations/Tasks by returning the fetched resource. | |
waitingContinuation.resume(returning: resource) | |
} | |
return resource | |
case .fetching: | |
let id = UUID() | |
return await withCheckedContinuation { continuation in | |
waitingContinuationsLocker.set(continuation, forId: id) | |
} | |
case let .value(resource): | |
return resource | |
} | |
} | |
} | |
var freshResource: Data? { | |
get async { | |
cachedResourceState = .none | |
return await resource | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment