A pretty common piece of work I'd like to generalize a Task version of is deduplicating work in callback logic by stashing the equivalent callbacks to be called when the shared underlying work is done. I have a fairly naive version of this working as it's own Task/actor/async/await soup
import Foundation
public actor Dupes<Key: Hashable, Success, Failure: Error> {
private var tasks = [Key:Task<Success, Failure>]()
}
public extension Dupes where Failure == Error {
func async(key: Key, priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) async throws -> Success {
if let task = tasks[key] {
return try await task.value
} else {
let task = Task(priority: priority) {
try await operation()
}
tasks[key] = task
let data = try await task.value
tasks.removeValue(forKey: key)
return data
}
}
}
public extension Dupes where Failure == Never {
func async(key: Key, priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success) async -> Success {
if let task = tasks[key] {
return await task.value
} else {
let task = Task(priority: priority) {
await operation()
}
tasks[key] = task
let data = await task.value
tasks.removeValue(forKey: key)
return data
}
}
}
That can used like so if we had an async method that returns a value (bar()) based on some work that we know is shared based on the identifier 123
let foo = Foo()
let dupes = Dupes<String, Int, Never>()
Task.detached {
await dupes.async(key: "123") {
await foo.bar()
}
}
Task.detached {
await dupes.async(key: "123") {
await foo.bar()
}
}
In naive testing this seems to work.
It feels really odd to be handing off to another actor for managing state that seems internal to whatever actor would need to avoid duplicate work.
I tried defining similar methods on a custom actor or on an extension on Actor but passing the storage via keyPath or inout violates the mutability rules of actors (which to be fair makes sense).
Is there an idiom for this kind of book keeping?
A good real world example of this is network requests where I'd pass in a Hashable network request that makes or owns a URLRequest for me to hand off to URLSession. I don't want to download the same request twice and I don't want to write the same book keeping logic again and again.