|
12 | 12 |
|
13 | 13 | import class Foundation.NSLock |
14 | 14 |
|
15 | | -/// Thread-safe value boxing structure |
| 15 | +/// Thread-safe value boxing structure that provides synchronized access to a wrapped value. |
16 | 16 | @dynamicMemberLookup |
17 | 17 | public final class ThreadSafeBox<Value> { |
18 | | - private var underlying: Value? |
| 18 | + private var underlying: Value |
19 | 19 | private let lock = NSLock() |
20 | 20 |
|
21 | | - public init() {} |
22 | | - |
| 21 | + /// Creates a new thread-safe box with the given initial value. |
| 22 | + /// |
| 23 | + /// - Parameter seed: The initial value to store in the box. |
23 | 24 | public init(_ seed: Value) { |
24 | 25 | self.underlying = seed |
25 | 26 | } |
26 | 27 |
|
27 | | - public func mutate(body: (Value?) throws -> Value?) rethrows { |
| 28 | + /// Atomically mutates the stored value by applying a transformation function. |
| 29 | + /// |
| 30 | + /// The transformation function receives the current value and returns a new value |
| 31 | + /// to replace it. The entire operation is performed under a lock to ensure atomicity. |
| 32 | + /// |
| 33 | + /// - Parameter body: A closure that takes the current value and returns a new value. |
| 34 | + /// - Throws: Any error thrown by the transformation function. |
| 35 | + public func mutate(body: (Value) throws -> Value) rethrows { |
28 | 36 | try self.lock.withLock { |
29 | 37 | let value = try body(self.underlying) |
30 | 38 | self.underlying = value |
31 | 39 | } |
32 | 40 | } |
33 | | - |
34 | | - public func mutate(body: (inout Value?) throws -> ()) rethrows { |
| 41 | + |
| 42 | + /// Atomically mutates the stored value by applying an in-place transformation. |
| 43 | + /// |
| 44 | + /// The transformation function receives an inout reference to the current value, |
| 45 | + /// allowing direct modification. The entire operation is performed under a lock |
| 46 | + /// to ensure atomicity. |
| 47 | + /// |
| 48 | + /// - Parameter body: A closure that receives an inout reference to the current value. |
| 49 | + /// - Throws: Any error thrown by the transformation function. |
| 50 | + public func mutate(body: (inout Value) throws -> Void) rethrows { |
35 | 51 | try self.lock.withLock { |
36 | 52 | try body(&self.underlying) |
37 | 53 | } |
38 | 54 | } |
39 | 55 |
|
40 | | - @discardableResult |
41 | | - public func memoize(body: () throws -> Value) rethrows -> Value { |
42 | | - if let value = self.get() { |
43 | | - return value |
44 | | - } |
45 | | - let value = try body() |
| 56 | + /// Atomically retrieves the current value from the box. |
| 57 | + /// |
| 58 | + /// - Returns: A copy of the current value stored in the box. |
| 59 | + public func get() -> Value { |
46 | 60 | self.lock.withLock { |
47 | | - self.underlying = value |
| 61 | + self.underlying |
48 | 62 | } |
49 | | - return value |
50 | 63 | } |
51 | 64 |
|
52 | | - @discardableResult |
53 | | - public func memoize(body: () async throws -> Value) async rethrows -> Value { |
54 | | - if let value = self.get() { |
55 | | - return value |
56 | | - } |
57 | | - let value = try await body() |
| 65 | + /// Atomically replaces the current value with a new value. |
| 66 | + /// |
| 67 | + /// - Parameter newValue: The new value to store in the box. |
| 68 | + public func put(_ newValue: Value) { |
58 | 69 | self.lock.withLock { |
59 | | - self.underlying = value |
| 70 | + self.underlying = newValue |
60 | 71 | } |
61 | | - return value |
62 | 72 | } |
63 | 73 |
|
64 | | - public func clear() { |
| 74 | + /// Provides thread-safe read-only access to properties of the wrapped value. |
| 75 | + /// |
| 76 | + /// This subscript allows you to access properties of the wrapped value using |
| 77 | + /// dot notation while maintaining thread safety. |
| 78 | + /// |
| 79 | + /// - Parameter keyPath: A key path to a property of the wrapped value. |
| 80 | + /// - Returns: The value of the specified property. |
| 81 | + public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T { |
65 | 82 | self.lock.withLock { |
66 | | - self.underlying = nil |
| 83 | + self.underlying[keyPath: keyPath] |
67 | 84 | } |
68 | 85 | } |
69 | 86 |
|
70 | | - public func get() -> Value? { |
71 | | - self.lock.withLock { |
72 | | - self.underlying |
| 87 | + /// Provides thread-safe read-write access to properties of the wrapped value. |
| 88 | + /// |
| 89 | + /// - Parameter keyPath: A writable key path to a property of the wrapped value. |
| 90 | + /// - Returns: The value of the specified property when getting. |
| 91 | + public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T { |
| 92 | + get { |
| 93 | + self.lock.withLock { |
| 94 | + self.underlying[keyPath: keyPath] |
| 95 | + } |
| 96 | + } |
| 97 | + set { |
| 98 | + self.lock.withLock { |
| 99 | + self.underlying[keyPath: keyPath] = newValue |
| 100 | + } |
73 | 101 | } |
74 | 102 | } |
| 103 | +} |
75 | 104 |
|
76 | | - public func get(default: Value) -> Value { |
77 | | - self.lock.withLock { |
78 | | - self.underlying ?? `default` |
79 | | - } |
| 105 | +// Extension for optional values to support empty initialization |
| 106 | +extension ThreadSafeBox { |
| 107 | + /// Creates a new thread-safe box initialized with nil for optional value types. |
| 108 | + /// |
| 109 | + /// This convenience initializer is only available when the wrapped value type is optional. |
| 110 | + public convenience init<Wrapped>() where Value == Wrapped? { |
| 111 | + self.init(nil) |
80 | 112 | } |
81 | 113 |
|
82 | | - public func put(_ newValue: Value) { |
| 114 | + /// Takes the stored optional value, setting it to nil. |
| 115 | + /// - Returns: The previously stored value, or nil if none was present. |
| 116 | + public func takeValue<Wrapped>() -> Value where Value == Wrapped? { |
83 | 117 | self.lock.withLock { |
84 | | - self.underlying = newValue |
| 118 | + guard let value = self.underlying else { return nil } |
| 119 | + self.underlying = nil |
| 120 | + return value |
85 | 121 | } |
86 | 122 | } |
87 | 123 |
|
88 | | - public func takeValue<U>() -> Value where U? == Value { |
| 124 | + /// Atomically sets the stored optional value to nil. |
| 125 | + /// |
| 126 | + /// This method is only available when the wrapped value type is optional. |
| 127 | + public func clear<Wrapped>() where Value == Wrapped? { |
89 | 128 | self.lock.withLock { |
90 | | - guard let value = self.underlying else { return nil } |
91 | 129 | self.underlying = nil |
92 | | - return value |
93 | 130 | } |
94 | 131 | } |
95 | 132 |
|
96 | | - public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T? { |
| 133 | + /// Atomically retrieves the stored value, returning a default if nil. |
| 134 | + /// |
| 135 | + /// This method is only available when the wrapped value type is optional. |
| 136 | + /// |
| 137 | + /// - Parameter defaultValue: The value to return if the stored value is nil. |
| 138 | + /// - Returns: The stored value if not nil, otherwise the default value. |
| 139 | + public func get<Wrapped>(default defaultValue: Wrapped) -> Wrapped where Value == Wrapped? { |
97 | 140 | self.lock.withLock { |
98 | | - self.underlying?[keyPath: keyPath] |
| 141 | + self.underlying ?? defaultValue |
99 | 142 | } |
100 | 143 | } |
101 | 144 |
|
102 | | - public subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T?>) -> T? { |
103 | | - get { |
104 | | - self.lock.withLock { |
105 | | - self.underlying?[keyPath: keyPath] |
| 145 | + /// Atomically computes and caches a value if not already present. |
| 146 | + /// |
| 147 | + /// If the box already contains a non-nil value, that value is returned immediately. |
| 148 | + /// Otherwise, the provided closure is executed to compute the value, which is then |
| 149 | + /// stored and returned. This method is only available when the wrapped value type is optional. |
| 150 | + /// |
| 151 | + /// - Parameter body: A closure that computes the value to store if none exists. |
| 152 | + /// - Returns: The cached value or the newly computed value. |
| 153 | + /// - Throws: Any error thrown by the computation closure. |
| 154 | + @discardableResult |
| 155 | + public func memoize<Wrapped>(body: () throws -> Wrapped) rethrows -> Wrapped where Value == Wrapped? { |
| 156 | + try self.lock.withLock { |
| 157 | + if let value = self.underlying { |
| 158 | + return value |
106 | 159 | } |
| 160 | + let value = try body() |
| 161 | + self.underlying = value |
| 162 | + return value |
107 | 163 | } |
108 | | - set { |
109 | | - self.lock.withLock { |
110 | | - if var value = self.underlying { |
111 | | - value[keyPath: keyPath] = newValue |
112 | | - } |
| 164 | + } |
| 165 | + |
| 166 | + /// Atomically computes and caches an optional value if not already present. |
| 167 | + /// |
| 168 | + /// If the box already contains a non-nil value, that value is returned immediately. |
| 169 | + /// Otherwise, the provided closure is executed to compute the value, which is then |
| 170 | + /// stored and returned. This method is only available when the wrapped value type is optional. |
| 171 | + /// |
| 172 | + /// If the returned value is `nil` subsequent calls to `memoize` or `memoizeOptional` will |
| 173 | + /// re-execute the closure. |
| 174 | + /// |
| 175 | + /// - Parameter body: A closure that computes the optional value to store if none exists. |
| 176 | + /// - Returns: The cached value or the newly computed value (which may be nil). |
| 177 | + /// - Throws: Any error thrown by the computation closure. |
| 178 | + @discardableResult |
| 179 | + public func memoizeOptional<Wrapped>(body: () throws -> Wrapped?) rethrows -> Wrapped? where Value == Wrapped? { |
| 180 | + try self.lock.withLock { |
| 181 | + if let value = self.underlying { |
| 182 | + return value |
113 | 183 | } |
| 184 | + let value = try body() |
| 185 | + self.underlying = value |
| 186 | + return value |
114 | 187 | } |
115 | 188 | } |
116 | 189 | } |
117 | 190 |
|
118 | 191 | extension ThreadSafeBox where Value == Int { |
| 192 | + /// Atomically increments the stored integer value by 1. |
| 193 | + /// |
| 194 | + /// This method is only available when the wrapped value type is Int. |
119 | 195 | public func increment() { |
120 | 196 | self.lock.withLock { |
121 | | - if let value = self.underlying { |
122 | | - self.underlying = value + 1 |
123 | | - } |
| 197 | + self.underlying = self.underlying + 1 |
124 | 198 | } |
125 | 199 | } |
126 | 200 |
|
| 201 | + /// Atomically decrements the stored integer value by 1. |
| 202 | + /// |
| 203 | + /// This method is only available when the wrapped value type is Int. |
127 | 204 | public func decrement() { |
128 | 205 | self.lock.withLock { |
129 | | - if let value = self.underlying { |
130 | | - self.underlying = value - 1 |
131 | | - } |
| 206 | + self.underlying = self.underlying - 1 |
132 | 207 | } |
133 | 208 | } |
134 | 209 | } |
135 | 210 |
|
136 | 211 | extension ThreadSafeBox where Value == String { |
| 212 | + /// Atomically appends a string to the stored string value. |
| 213 | + /// |
| 214 | + /// This method is only available when the wrapped value type is String. |
| 215 | + /// |
| 216 | + /// - Parameter value: The string to append to the current stored value. |
137 | 217 | public func append(_ value: String) { |
138 | 218 | self.mutate { existingValue in |
139 | | - if let existingValue { |
140 | | - return existingValue + value |
141 | | - } else { |
| 219 | + existingValue + value |
| 220 | + } |
| 221 | + } |
| 222 | +} |
| 223 | + |
| 224 | +extension ThreadSafeBox: @unchecked Sendable where Value: Sendable {} |
| 225 | + |
| 226 | +/// Thread-safe value boxing structure that provides synchronized asynchronous memoization of a wrapped value. |
| 227 | +public final class AsyncMemoizableThreadSafeBox<Value: Sendable>: @unchecked Sendable { |
| 228 | + private var underlying: Value? |
| 229 | + private let lock = NSLock() |
| 230 | + private let asyncCoordination = AsyncMemoizationCoordinator<Value>() |
| 231 | + |
| 232 | + public init() {} |
| 233 | + |
| 234 | + /// Atomically retrieves the current value from the box. |
| 235 | + /// |
| 236 | + /// - Returns: The current value stored in the box, or nil if none is present. |
| 237 | + public func get() -> Value? { |
| 238 | + self.lock.withLock { |
| 239 | + self.underlying |
| 240 | + } |
| 241 | + } |
| 242 | + |
| 243 | + /// Atomically computes and caches a value produced by an async function, if not already present. |
| 244 | + /// |
| 245 | + /// If the box already contains a non-nil value that value is returned immediately. |
| 246 | + /// Otherwise, the provided async closure is executed to compute the value, which is then |
| 247 | + /// stored and returned. |
| 248 | + /// |
| 249 | + /// Concurrent calls to memoize will wait for the first call to complete and receive its result. |
| 250 | + /// If the body throws an error, all pending calls receive that error and the state is reset. |
| 251 | + /// |
| 252 | + /// - Parameter body: An async closure that computes the value to store if none exists. |
| 253 | + /// - Returns: The cached value or the newly computed value. |
| 254 | + /// - Throws: Any error thrown by the computation closure. |
| 255 | + @discardableResult |
| 256 | + public func memoize(body: @Sendable @escaping () async throws -> Value) async throws -> Value { |
| 257 | + if let value = self.get() { |
| 258 | + return value |
| 259 | + } |
| 260 | + |
| 261 | + // Try to register as the executor, or get the existing task |
| 262 | + let task: Task<Value, Error> = await self.asyncCoordination.getOrCreateTask { |
| 263 | + // This closure is only called by the first caller |
| 264 | + Task<Value, Error> { |
| 265 | + // Double-check after acquiring coordination |
| 266 | + if let value = self.get() { |
| 267 | + return value |
| 268 | + } |
| 269 | + |
| 270 | + let value = try await body() |
| 271 | + |
| 272 | + // Store the value |
| 273 | + self.lock.withLock { |
| 274 | + self.underlying = value |
| 275 | + } |
142 | 276 | return value |
143 | 277 | } |
144 | 278 | } |
| 279 | + |
| 280 | + // Everyone (including the first caller) awaits the same task |
| 281 | + do { |
| 282 | + let result = try await task.value |
| 283 | + await self.asyncCoordination.clearTask() |
| 284 | + return result |
| 285 | + } catch { |
| 286 | + await self.asyncCoordination.clearTask() |
| 287 | + throw error |
| 288 | + } |
| 289 | + } |
| 290 | + |
| 291 | + // Actor for coordinating async memoization within a thread safe box |
| 292 | + private actor AsyncMemoizationCoordinator<T: Sendable>: Sendable { |
| 293 | + private var inProgressTask: Task<T, Error>? |
| 294 | + |
| 295 | + /// Returns an existing task if one is in progress, or creates and stores a new one |
| 296 | + func getOrCreateTask(_ createTask: @Sendable () -> Task<T, Error>) -> Task<T, Error> { |
| 297 | + if let existingTask = inProgressTask { |
| 298 | + return existingTask |
| 299 | + } |
| 300 | + |
| 301 | + // We're the first - create and store the task |
| 302 | + let task = createTask() |
| 303 | + inProgressTask = task |
| 304 | + return task |
| 305 | + } |
| 306 | + |
| 307 | + /// Clears the current task |
| 308 | + func clearTask() { |
| 309 | + inProgressTask = nil |
| 310 | + } |
145 | 311 | } |
146 | 312 | } |
147 | 313 |
|
148 | | -extension ThreadSafeBox: @unchecked Sendable where Value: Sendable {} |
|
0 commit comments