@@ -76,9 +76,9 @@ import ReactiveSwift
7676/// ### Thread safety
7777///
7878/// The `Store` class is not thread-safe, and so all interactions with an instance of ``Store``
79- /// (including all of its scopes and derived ``ViewStore``s) must be done on the same thread.
80- /// Further, if the store is powering a SwiftUI or UIKit view, as is customary, then all
81- /// interactions must be done on the _main_ thread.
79+ /// (including all of its scopes and derived ``ViewStore``s) must be done on the same thread the
80+ /// store was created on. Further, if the store is powering a SwiftUI or UIKit view, as is
81+ /// customary, then all interactions must be done on the _main_ thread.
8282///
8383/// The reason stores are not thread-safe is due to the fact that when an action is sent to a store,
8484/// a reducer is run on the current state, and this process cannot be done from multiple threads.
@@ -111,6 +111,21 @@ import ReactiveSwift
111111/// However, by leaving scheduling out of the ``Store`` we get to test these aspects of our effects
112112/// if we so desire, or we can ignore if we prefer. We have that flexibility.
113113///
114+ /// #### Thread safety checks
115+ ///
116+ /// The store performs some basic thread safety checks in order to help catch mistakes. Stores
117+ /// constructed via the initializer ``Store/init(initialState:reducer:environment:)`` are assumed
118+ /// to run only on the main thread, and so a check is executed immediately to make sure that is the
119+ /// case. Further, all actions sent to the store and all scopes (see ``Store/scope(state:action:)``)
120+ /// of the store are also checked to make sure that work is performed on the main thread.
121+ ///
122+ /// If you need a store that runs on a non-main thread, which should be very rare and you should
123+ /// have a very good reason to do so, then you can construct a store via the
124+ /// ``Store/unchecked(initialState:reducer:environment:)`` static method to opt out of all main
125+ /// thread checks.
126+ ///
127+ /// ---
128+ ///
114129/// See also: ``ViewStore`` to understand how one observes changes to the state in a ``Store`` and
115130/// sends user actions.
116131public final class Store < State, Action> {
@@ -130,7 +145,7 @@ public final class Store<State, Action> {
130145 private let reducer : ( inout State , Action ) -> Effect < Action , Never >
131146 private var bufferedActions : [ Action ] = [ ]
132147 #if DEBUG
133- private let initialThread = Thread . current
148+ private let mainQueueChecksEnabled : Bool
134149 #endif
135150
136151 /// Initializes a store from an initial state, a reducer, and an environment.
@@ -139,13 +154,38 @@ public final class Store<State, Action> {
139154 /// - initialState: The state to start the application in.
140155 /// - reducer: The reducer that powers the business logic of the application.
141156 /// - environment: The environment of dependencies for the application.
142- public init < Environment> (
157+ public convenience init < Environment> (
143158 initialState: State ,
144159 reducer: Reducer < State , Action , Environment > ,
145160 environment: Environment
146161 ) {
147- self . state = initialState
148- self . reducer = { state, action in reducer. run ( & state, action, environment) }
162+ self . init (
163+ initialState: initialState,
164+ reducer: reducer,
165+ environment: environment,
166+ mainQueueChecksEnabled: true
167+ )
168+ self . threadCheck ( status: . `init`)
169+ }
170+
171+ /// Initializes a store from an initial state, a reducer, and an environment, and the main thread
172+ /// check is disabled for all interactions with this store.
173+ ///
174+ /// - Parameters:
175+ /// - initialState: The state to start the application in.
176+ /// - reducer: The reducer that powers the business logic of the application.
177+ /// - environment: The environment of dependencies for the application.
178+ public static func unchecked< Environment> (
179+ initialState: State ,
180+ reducer: Reducer < State , Action , Environment > ,
181+ environment: Environment
182+ ) -> Self {
183+ Self (
184+ initialState: initialState,
185+ reducer: reducer,
186+ environment: environment,
187+ mainQueueChecksEnabled: false
188+ )
149189 }
150190
151191 /// Scopes the store to one that exposes local state and actions.
@@ -325,8 +365,8 @@ public final class Store<State, Action> {
325365 self . scope ( state: toLocalState, action: { $0 } )
326366 }
327367
328- func send( _ action: Action , isFromViewStore : Bool = true ) {
329- self . threadCheck ( status: . send( action, isFromViewStore : isFromViewStore ) )
368+ func send( _ action: Action , originatingFrom originatingAction : Action ? = nil ) {
369+ self . threadCheck ( status: . send( action, originatingAction : originatingAction ) )
330370
331371 self . bufferedActions. append ( action)
332372 guard !self . isSending else { return }
@@ -345,8 +385,8 @@ public final class Store<State, Action> {
345385 var didComplete = false
346386 let uuid = UUID ( )
347387 let observer = Signal < Action , Never > . Observer (
348- value: { [ weak self] action in
349- self ? . send ( action , isFromViewStore : false )
388+ value: { [ weak self] effectAction in
389+ self ? . send ( effectAction , originatingFrom : action )
350390 } ,
351391 completed: { [ weak self] in
352392 self ? . threadCheck ( status: . effectCompletion( action) )
@@ -386,63 +426,97 @@ public final class Store<State, Action> {
386426
387427 private enum ThreadCheckStatus {
388428 case effectCompletion( Action )
429+ case `init`
389430 case scope
390- case send( Action , isFromViewStore : Bool )
431+ case send( Action , originatingAction : Action ? )
391432 }
392433
393434 @inline ( __always)
394435 private func threadCheck( status: ThreadCheckStatus ) {
395436 #if DEBUG
396- guard self . initialThread != Thread . current
437+ guard self . mainQueueChecksEnabled && !isMainQueue
397438 else { return }
398439
399440 let message : String
400441 switch status {
401442 case let . effectCompletion( action) :
402443 message = """
403- An effect returned from the action " \( debugCaseOutput ( action) ) " completed on the wrong \
444+ An effect returned from the action " \( debugCaseOutput ( action) ) " completed on a non-main \
404445 thread. Make sure to use " .receive(on:) " on any effects that execute on background \
405- threads to receive their output on the same thread the store was created on.
446+ threads to receive their output on the main thread, or create this store via \
447+ " Store.unchecked " to disable the main thread checker.
448+ """
449+
450+ case . `init`:
451+ message = """
452+ " Store.init " was called on a non-main thread. Make sure that stores are initialized on \
453+ the main thread, or create this store via " Store.unchecked " to disable the main thread \
454+ checker.
406455 """
407456
408457 case . scope:
409458 message = """
410- " Store.scope " was called on the wrong thread. Make sure that " Store.scope " is always \
411- called on the same thread the store was created on.
459+ " Store.scope " was called on a non-main thread. Make sure that " Store.scope " is always \
460+ called on the main thread, or create this store via " Store.unchecked " to disable the \
461+ main thread checker.
412462 """
413463
414- case let . send( action, isFromViewStore : true ) :
464+ case let . send( action, originatingAction : nil ) :
415465 message = """
416- " ViewStore.send( \( debugCaseOutput ( action) ) ) " was called on the wrong thread. Make sure \
417- that " ViewStore.send " is always called on the same thread the store was created on.
466+ " ViewStore.send( \( debugCaseOutput ( action) ) ) " was called on a non-main thread. Make sure \
467+ that " ViewStore.send " is always called on the main thread, or create this store via \
468+ " Store.unchecked " to disable the main thread checker.
418469 """
419470
420- case let . send( action, isFromViewStore : false ) :
471+ case let . send( action, originatingAction : . some ( originatingAction ) ) :
421472 message = """
422- An effect emitted the action " \( debugCaseOutput ( action) ) " from the wrong thread. Make \
423- sure to use " .receive(on:) " on any effects that execute on background threads to receive \
424- their output on the same thread the store was created on.
473+ An effect returned from " \( debugCaseOutput ( originatingAction) ) " emitted the action \
474+ " \( debugCaseOutput ( action) ) " on a non-main thread. Make sure to use " .receive(on:) " on \
475+ any effects that execute on background threads to receive their output on the main \
476+ thread, or create this store via " Store.unchecked " to disable the main thread checker.
425477 """
426478 }
427-
479+
428480 breakpoint (
429481 """
430482 ---
431483 Warning:
432484
433- The store was interacted with on a thread that is different from the thread the store was \
434- created on:
485+ A store created on the main thread was interacted with on a non-main thread:
435486
436- \( message )
487+ Thread: \( Thread . current )
437488
438- Store created on: \( self . initialThread)
439- Action sent on: \( Thread . current)
489+ \( message)
440490
441491 The " Store " class is not thread-safe, and so all interactions with an instance of " Store " \
442- (including all of its scopes and derived view stores) must be done on the same thread.
492+ (including all of its scopes and derived view stores) must be done on the main thread.
443493 ---
444494 """
445495 )
446496 #endif
447497 }
498+
499+ private init < Environment> (
500+ initialState: State ,
501+ reducer: Reducer < State , Action , Environment > ,
502+ environment: Environment ,
503+ mainQueueChecksEnabled: Bool
504+ ) {
505+ self . state = initialState
506+ self . reducer = { state, action in reducer. run ( & state, action, environment) }
507+
508+ #if DEBUG
509+ self . mainQueueChecksEnabled = mainQueueChecksEnabled
510+ #endif
511+ }
512+ }
513+
514+ private let mainQueueKey = DispatchSpecificKey < UInt8 > ( )
515+ private let mainQueueValue : UInt8 = 0
516+ private var isMainQueue : Bool {
517+ _ = setSpecific
518+ return DispatchQueue . getSpecific ( key: mainQueueKey) == mainQueueValue
448519}
520+ private var setSpecific : ( ) = {
521+ DispatchQueue . main. setSpecific ( key: mainQueueKey, value: mainQueueValue)
522+ } ( )
0 commit comments