1- /// Actor-backed registry that deduplicates concurrent async operations by key.
1+ /// An actor-isolated registry that deduplicates concurrent async operations by key.
22///
3- /// `SingleFlightRegistry` maintains a map of in-flight operations keyed by
4- /// a composite of the caller-supplied key and the expected result type.
5- /// When a call arrives for a key already in flight, it awaits the existing
6- /// task rather than starting a new one.
3+ /// `SingleFlightRegistry` is the engine behind ``withSingleFlight(key:operation:)``.
4+ /// It maintains a map of in-flight operations keyed by a composite of the
5+ /// caller-supplied key and the expected result type. When a call arrives for
6+ /// a key already in flight, it joins the existing operation rather than
7+ /// starting a redundant one — all callers suspend and receive the same result.
78///
8- /// This type is internal. Public access is via ``withSingleFlight(key:operation:)``.
9+ /// ## Type safety
10+ ///
11+ /// Each entry in the registry is stored as a `Flight<T>` — a generic,
12+ /// type-preserving box that holds a `Task<T, Error>`. The composite
13+ /// `FlightKey` encodes both the caller's key and `ObjectIdentifier(T.self)`,
14+ /// preventing collisions between callers sharing the same string key but
15+ /// expecting different return types. Joining an existing flight downcasts
16+ /// `AnyFlight → Flight<T>`, which is guaranteed safe by the `typeID` match.
17+ /// No force-unwrapped casts appear at any call site.
18+ ///
19+ /// ## Lifecycle
20+ ///
21+ /// A flight entry is inserted when the first caller for a given key arrives
22+ /// and removed — via `defer` — when that operation completes, fails, or is
23+ /// cancelled. Subsequent callers for the same key therefore always start a
24+ /// fresh operation rather than joining a stale one.
25+ ///
26+ /// ## Thread safety
27+ ///
28+ /// All state mutations are actor-isolated. No external synchronization is
29+ /// required. Call sites may be on any actor or unstructured task context.
30+ ///
31+ /// - Note: This type is internal. All public access is through
32+ /// ``withSingleFlight(key:operation:)``.
933internal actor SingleFlightRegistry {
1034
11- // MARK: - Shared
12-
1335 static let shared = SingleFlightRegistry ( )
1436
37+ // MARK: - Type-preserving flight box
38+
39+ /// A type-erased protocol that allows heterogeneous storage of `Flight<T>`
40+ /// values in a single dictionary without losing the ability to await them.
41+ private protocol AnyFlight : AnyObject {
42+ /// Awaits the underlying task and returns its value erased to `Any`.
43+ func awaitResult( ) async throws -> Any
44+ }
45+
46+ /// A concrete, type-preserving wrapper around a `Task<T, Error>`.
47+ ///
48+ /// Storing `Task<T, Error>` directly — rather than erasing to `Task<Any, Error>`
49+ /// — eliminates all force casts at join sites. The `Flight<T>` is recovered
50+ /// from the registry via `as? Flight<T>`, which is guaranteed to succeed
51+ /// when `FlightKey.typeID` matches.
52+ private final class Flight < T: Sendable > : AnyFlight {
53+ let task : Task < T , Error >
54+ init ( _ task: Task < T , Error > ) { self . task = task }
55+ func awaitResult( ) async throws -> Any { try await task. value }
56+ }
57+
1558 // MARK: - Storage
1659
17- /// Composite key preventing type collisions across callers sharing the same
18- /// string key but expecting different return types.
60+ /// A composite key that uniquely identifies an in-flight operation by
61+ /// both its caller-supplied key and its expected return type.
62+ ///
63+ /// Including `typeID` prevents a caller expecting `String` from joining
64+ /// a flight started by a caller expecting `Int` under the same string key.
1965 private struct FlightKey : Hashable {
66+ /// The caller-supplied key, type-erased to `AnyHashable`.
2067 let key : AnyHashable
68+ /// The `ObjectIdentifier` of the expected return type `T`.
2169 let typeID : ObjectIdentifier
2270 }
2371
24- private var flights : [ FlightKey : Task < Any , Error > ] = [ : ]
72+ private var flights : [ FlightKey : AnyFlight ] = [ : ]
2573
2674 // MARK: - Execute
2775
28- /// Joins an existing flight or starts a new one for the given key and type .
76+ /// Joins an existing in- flight operation or starts a new one.
2977 ///
30- /// If a flight for `(key, T)` is already in progress, the caller suspends
31- /// and awaits its result. Otherwise a new task is started, recorded, and
32- /// awaited. The entry is removed when the task finishes — on success,
33- /// failure, or cancellation.
78+ /// If an operation for `(key, T)` is already in progress, the caller
79+ /// suspends and receives its result when it completes. Otherwise a new
80+ /// `Task<T, Error>` is created, stored under `flightKey`, and awaited.
81+ /// The entry is removed from the registry when the task finishes —
82+ /// whether by success, failure, or cancellation.
3483 ///
3584 /// - Parameters:
36- /// - key: The caller-supplied hashable key.
37- /// - operation: The work to perform if no flight is currently running.
38- /// - Returns: The result produced by the in-flight or newly started operation.
85+ /// - key: A `Hashable & Sendable` value identifying the operation.
86+ /// - operation: The async throwing closure to execute if no flight
87+ /// is currently in progress for this key and return type.
88+ /// - Returns: The value produced by the in-flight or newly started operation,
89+ /// shared across all concurrent callers for the same key.
3990 /// - Throws: Rethrows any error from the operation to all waiting callers.
4091 func execute< Key: Hashable & Sendable , T: Sendable > (
4192 key: Key ,
@@ -46,33 +97,32 @@ internal actor SingleFlightRegistry {
4697 typeID: ObjectIdentifier ( T . self)
4798 )
4899
49- // Join existing flight
50- if let existing = flights [ flightKey] {
100+ // Join existing flight — the downcast to Flight<T> is guaranteed safe
101+ // because FlightKey.typeID encodes T, so only a Flight<T> can be stored
102+ // under this key. No force cast is required.
103+ if let existing = flights [ flightKey] as? Flight < T > {
51104 Diagnostics . log ( " withSingleFlight.joined " , key: String ( describing: key) )
52- let value = try await existing. value
53- return value as! T // safe: guaranteed by FlightKey.typeID
105+ return try await existing. task. value
54106 }
55107
56- // Start new flight
108+ // No flight in progress — start one and register it before suspending
109+ // so that concurrent callers arriving during the first await join it.
57110 Diagnostics . log ( " withSingleFlight.started " , key: String ( describing: key) )
58111
59- let task = Task < Any , Error > {
60- try await operation ( )
61- }
112+ let task = Task < T , Error > { try await operation ( ) }
113+ flights [ flightKey] = Flight ( task)
62114
63- flights [ flightKey] = task
64115 defer {
65116 flights [ flightKey] = nil
66117 Diagnostics . log ( " withSingleFlight.completed " , key: String ( describing: key) )
67118 }
68119
69- let value = try await task. value
70- return value as! T // safe: guaranteed by FlightKey.typeID
120+ return try await task. value
71121 }
72122
73- /// Returns the number of distinct operations currently in flight.
123+ /// The number of distinct operations currently in flight.
74124 ///
75- /// Intended for diagnostics and tests only.
125+ /// Intended for diagnostics and unit tests only. Not part of the public API .
76126 func inFlightCount( ) -> Int {
77127 flights. count
78128 }
0 commit comments