Created
January 17, 2023 14:02
-
-
Save erwinmaza/5ef34726de9809290596477dafe5f7e3 to your computer and use it in GitHub Desktop.
Xcode playground code sample showing how to batch and de-dupe inflight network calls using Swift concurrency
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
import Foundation | |
func threadNumberForThread() -> String { | |
let array1 = Thread.current.description.components(separatedBy: ">") | |
if array1.count > 1 { | |
let array2 = array1[1].trimmingCharacters(in: CharacterSet(charactersIn: "{}")).components(separatedBy: ",") | |
for pair in array2 { | |
let array3 = pair.components(separatedBy: "=") | |
if array3.count > 1 { | |
if array3[0].contains("number") { | |
return array3[1].trimmingCharacters(in: CharacterSet.whitespaces) | |
} | |
} | |
} | |
} | |
return "(unknown)" | |
} | |
struct ResponseObject: Decodable { let name: String } | |
struct Test { | |
typealias ContinuationsMap = [String: [CheckedContinuation<(Decodable?, Error?), Never>]] | |
private static var pendingContinuations = ContinuationsMap() | |
static | |
func getObjectAtEndpoint<T: Decodable>(_ endpoint: String, expectingObjectOfType: T.Type) async -> (T?, Error?) { | |
print("enter getObjectAtEndpoint \(endpoint), thread: \(threadNumberForThread())") | |
// first call to this endpoint will skip this if condition | |
if var continuations = pendingContinuations[endpoint] { | |
print("get from \(endpoint) in flight, creating continuation") | |
// subsequent calls to same endpoint get parked here to await results from first call | |
let response = await withCheckedContinuation({ continuation in | |
continuations.append(continuation) | |
pendingContinuations[endpoint] = continuations | |
}) | |
return (response.0 as? T, response.1) | |
} | |
// creating this key signals that this endpoint is in flight | |
pendingContinuations[endpoint] = [] | |
print("calling fetchFromAPI for \(endpoint)") | |
let object = await fetchFromAPI(endpoint, expectingObjectOfType: T.self) | |
// this is always executed even if there are no pending subsequent calls | |
if let continuations = pendingContinuations[endpoint] { | |
print("resuming \(continuations.count) continuations, thread: \(threadNumberForThread())") | |
// each call that was "parked" above will now get the same set of results | |
continuations.forEach({ $0.resume(returning: object) }) | |
// remove key from continuation map, signaling all results have been returned for this endpoint | |
pendingContinuations.removeValue(forKey: endpoint) | |
} | |
return object | |
} | |
private static | |
func fetchFromAPI<T: Decodable>(_ endpoint: String, expectingObjectOfType: T.Type) async -> (T?, Error?) { | |
let response = await withCheckedContinuation { continuation in | |
sleep(10) | |
print("done fetching from \(endpoint) and decoding response. No errors!") | |
let object = ResponseObject(name: endpoint) | |
let error: Error? = nil | |
continuation.resume(returning: (object, error)) | |
} | |
return (response.0 as? T, response.1) | |
} | |
} | |
Task(priority: .userInitiated) { | |
let objects = await Test.getObjectAtEndpoint("a", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(1) | |
Task(priority: .background) { | |
let objects = await Test.getObjectAtEndpoint("b", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(1) | |
Task(priority: .utility) { | |
let objects = await Test.getObjectAtEndpoint("b", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(1) | |
Task(priority: .low) { | |
let objects = await Test.getObjectAtEndpoint("a", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(1) | |
Task(priority: .medium) { | |
let objects = await Test.getObjectAtEndpoint("c", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(1) | |
Task(priority: .high) { | |
let objects = await Test.getObjectAtEndpoint("b", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(1) | |
Task(priority: .utility) { | |
let objects = await Test.getObjectAtEndpoint("a", expectingObjectOfType: ResponseObject.self) | |
} | |
sleep(12) | |
Task(priority: .background) { | |
let objects = await Test.getObjectAtEndpoint("b", expectingObjectOfType: ResponseObject.self) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment