1111
1212import Combine
1313import Foundation
14+ import os
1415
1516/// `_BaseMiddleware` is an open class that serves as the base for creating middleware components
1617/// in the UDF architecture. Middleware is responsible for handling side effects and can process actions
@@ -63,7 +64,17 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
6364 public typealias ErrorMapper < Id> = @Sendable ( _ id: Id , _ error: Error ) -> any Action
6465
6566 /// A dictionary to track ongoing tasks by their unique identifiers, allowing for cancellation.
66- public var cancellations : [ AnyHashable : CancellableTask ] = [ : ]
67+ public var cancellations : [ AnyHashable : CancellableTask ] {
68+ cancellationsBox. withLockUnchecked { box in
69+ box. cancellations
70+ }
71+ }
72+
73+ /// Synchronizes access to the middleware's mutable state.
74+ ///
75+ /// This property ensures that operations on the internal data such as reading/writing—are
76+ /// atomic across different physical threads, preventing data races and memory corruption.
77+ private let cancellationsBox = OSAllocatedUnfairLock ( initialState: CancellationsBox ( ) )
6778
6879 // MARK: - Cancellation
6980
@@ -80,7 +91,9 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
8091 }
8192
8293 cancellableTask. cancel ( )
83- cancellations [ anyId] = nil
94+ cancellationsBox. withLockUnchecked { box in
95+ box. removeCancellation ( forKey: anyId)
96+ }
8497 return true
8598 }
8699
@@ -140,17 +153,21 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
140153 let testGroupKey = TestGroup . enter ( for: store)
141154
142155 // Subscribe to the effect and store the cancellation token
143- cancellations [ anyId ] = effect
156+ let cancellable = effect
144157 . subscribe ( on: queue)
145158 . receive ( on: queue)
146159 . handleEvents ( receiveCancel: { [ weak self] in
147160 // Handle cancellation: Remove the task from cancellations and dispatch cancellation action
148- self ? . cancellations [ anyId] = nil
161+ self ? . cancellationsBox. withLockUnchecked { box in
162+ box. removeCancellation ( forKey: anyId)
163+ }
149164 self ? . dispatch ( action: mapAction ( Actions . DidCancelEffect ( by: cancellation) ) , filePosition: filePosition)
150165 } )
151166 . sink ( receiveCompletion: { [ weak self] _ in
152167 // Handle completion: Remove the task from cancellations and signal Testing
153- self ? . cancellations [ anyId] = nil
168+ self ? . cancellationsBox. withLockUnchecked { box in
169+ box. removeCancellation ( forKey: anyId)
170+ }
154171 } , receiveValue: { [ weak self] action in
155172 // Handle receiving a value: Dispatch the action to the store
156173 if self ? . cancellations [ anyId] != nil {
@@ -159,6 +176,9 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
159176 TestGroup . instanceFor ( key: testGroupKey) . leave ( )
160177 }
161178 } )
179+ cancellationsBox. withLockUnchecked { state in
180+ state. set ( cancellable: cancellable, forKey: anyId)
181+ }
162182 }
163183
164184 /// Executes an effect that conforms to both `PureEffect` and `ErasableToEffect` and dispatches actions to the store.
@@ -240,12 +260,14 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
240260 let testGroupKey = TestGroup . instanceKey ( store)
241261
242262 // Subscribe to the effect and store the cancellation token
243- cancellations [ anyId ] = effect
263+ let cancellable = effect
244264 . subscribe ( on: queue)
245265 . receive ( on: queue)
246266 . handleEvents ( receiveCancel: { [ weak self] in
247267 // Handle cancellation: Remove the task from cancellations and dispatch cancellation action
248- self ? . cancellations [ anyId] = nil
268+ self ? . cancellationsBox. withLockUnchecked { box in
269+ box. removeCancellation ( forKey: anyId)
270+ }
249271 self ? . dispatch ( action: mapAction ( Actions . DidCancelEffect ( by: cancellation) ) , filePosition: filePosition)
250272 } )
251273 . flatMap { [ weak self] action in
@@ -263,7 +285,9 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
263285 }
264286 . sink ( receiveCompletion: { [ weak self] _ in
265287 // Handle completion: Remove the task from cancellations
266- self ? . cancellations [ anyId] = nil
288+ self ? . cancellationsBox. withLockUnchecked { box in
289+ box. removeCancellation ( forKey: anyId)
290+ }
267291 TestGroup . instanceFor ( key: testGroupKey) . leave ( )
268292
269293 } , receiveValue: { [ weak self] result in
@@ -272,6 +296,9 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
272296 self ? . dispatch ( action: mapAction ( result. action) , filePosition: filePosition)
273297 }
274298 } )
299+ cancellationsBox. withLockUnchecked { box in
300+ box. set ( cancellable: cancellable, forKey: anyId)
301+ }
275302 }
276303
277304 /// Runs a `PureEffect` and dispatches its actions to the store.
@@ -314,17 +341,21 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
314341 let testGroupKey = TestGroup . instanceKey ( store)
315342
316343 // Subscribe to the effect and store the cancellation token
317- cancellations [ anyId ] = effect
344+ let cancellable = effect
318345 . subscribe ( on: queue) // Subscribe to the effect on the specified queue
319346 . receive ( on: queue) // Specify the queue on which to receive events
320347 . handleEvents ( receiveCancel: { [ weak self] in
321348 // Handle cancellation: Remove the task from cancellations and dispatch cancellation action
322- self ? . cancellations [ anyId] = nil
349+ self ? . cancellationsBox. withLockUnchecked { box in
350+ box. removeCancellation ( forKey: anyId)
351+ }
323352 self ? . dispatch ( action: mapAction ( Actions . DidCancelEffect ( by: cancellation) ) , filePosition: filePosition )
324353 } )
325354 . sink ( receiveCompletion: { [ weak self] _ in
326355 // Handle completion: Remove the task from cancellations
327- self ? . cancellations [ anyId] = nil
356+ self ? . cancellationsBox. withLockUnchecked { box in
357+ box. removeCancellation ( forKey: anyId)
358+ }
328359 TestGroup . instanceFor ( key: testGroupKey) . leave ( )
329360
330361 } , receiveValue: { [ weak self] action in
@@ -334,6 +365,9 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
334365 self ? . dispatch ( action: mapAction ( action) , filePosition: filePosition)
335366 }
336367 } )
368+ cancellationsBox. withLockUnchecked { box in
369+ box. set ( cancellable: cancellable, forKey: anyId)
370+ }
337371 }
338372
339373 // MARK: - Concurrency
@@ -464,13 +498,31 @@ open class _BaseMiddleware<State: AppReducer>: _Middleware, @unchecked Sendable
464498 }
465499
466500 // Remove the task from the cancellations dictionary
467- _ = self ? . queue . sync { [ weak self ] in
468- self ? . cancellations . removeValue ( forKey: anyCancellationId)
501+ self ? . cancellationsBox . withLockUnchecked { box in
502+ box . removeCancellation ( forKey: anyCancellationId)
469503 }
470504 }
471505
472506 // Store the task in the cancellations dictionary for future cancellation
473- cancellations [ anyCancellationId] = task
507+ cancellationsBox. withLockUnchecked { box in
508+ box. set ( cancellable: task, forKey: anyCancellationId)
509+ }
510+ }
511+
512+ /// A container for the middleware's mutable state, designed to be managed by a synchronization mechanism
513+ ///
514+ /// This class enables the safe retrieval and cancellation of tasks across different threads,
515+ /// ensuring that internal storage is modified only through the established lock.
516+ private class CancellationsBox : @unchecked Sendable {
517+ var cancellations : [ AnyHashable : CancellableTask ] = [ : ]
518+
519+ func set( cancellable: CancellableTask , forKey key: AnyHashable ) {
520+ cancellations [ key] = cancellable
521+ }
522+
523+ func removeCancellation( forKey key: AnyHashable ) {
524+ cancellations. removeValue ( forKey: key)
525+ }
474526 }
475527}
476528
0 commit comments