@@ -120,6 +120,9 @@ public final class Store<State, Action> {
120120 private var parentCancellable : AnyCancellable ?
121121 private let reducer : ( inout State , Action ) -> Effect < Action , Never >
122122 private var bufferedActions : [ Action ] = [ ]
123+ #if DEBUG
124+ private var initialThread = Thread . current
125+ #endif
123126
124127 /// Initializes a store from an initial state, a reducer, and an environment.
125128 ///
@@ -322,7 +325,6 @@ public final class Store<State, Action> {
322325 action fromLocalAction: @escaping ( LocalAction ) -> Action
323326 ) -> AnyPublisher < Store < LocalState , LocalAction > , Never >
324327 where P. Output == LocalState , P. Failure == Never {
325-
326328 func extractLocalState( _ state: State ) -> LocalState ? {
327329 var localState : LocalState ?
328330 _ = toLocalState ( Just ( state) . eraseToAnyPublisher ( ) )
@@ -365,7 +367,9 @@ public final class Store<State, Action> {
365367 self . publisherScope ( state: toLocalState, action: { $0 } )
366368 }
367369
368- func send( _ action: Action ) {
370+ func send( _ action: Action , isFromViewStore: Bool = true ) {
371+ self . threadCheck ( status: . send( action, isFromViewStore: isFromViewStore) )
372+
369373 self . bufferedActions. append ( action)
370374 guard !self . isSending else { return }
371375
@@ -382,45 +386,14 @@ public final class Store<State, Action> {
382386
383387 var didComplete = false
384388 let uuid = UUID ( )
385-
386- #if DEBUG
387- let initalThread = Thread . current
388- initalThread. threadDictionary [ uuid] = true
389- #endif
390-
391389 let effectCancellable = effect. sink (
392390 receiveCompletion: { [ weak self] _ in
393- #if DEBUG
394- if Thread . current. threadDictionary [ uuid] == nil {
395- breakpoint (
396- """
397- ---
398- Warning: Store.send
399-
400- The Store class is not thread-safe, and so all interactions with an instance of Store
401- (including all of its scopes and derived ViewStores) must be done on the same thread.
402-
403- \( debugCaseOutput ( action) ) has produced an Effect that was completed on a different thread \
404- from the one it was executed on.
405-
406- Starting thread: \( initalThread)
407- Final thread: \( Thread . current)
408-
409- Possible fixes for this are:
410-
411- * Add a .receive(on:) to the Effect to ensure it completes on this Stores correct thread.
412- """
413- )
414- }
415-
416- Thread . current. threadDictionary [ uuid] = nil
417- #endif
418-
391+ self ? . threadCheck ( status: . effectCompletion( action) )
419392 didComplete = true
420393 self ? . effectCancellables [ uuid] = nil
421394 } ,
422395 receiveValue: { [ weak self] action in
423- self ? . send ( action)
396+ self ? . send ( action, isFromViewStore : false )
424397 }
425398 )
426399
@@ -440,4 +413,59 @@ public final class Store<State, Action> {
440413 func absurd< A> ( _ never: Never ) -> A { }
441414 return self . scope ( state: { $0 } , action: absurd)
442415 }
416+
417+ private enum ThreadCheckStatus {
418+ case effectCompletion( Action )
419+ case send( Action , isFromViewStore: Bool )
420+ }
421+
422+ @inline ( __always)
423+ private func threadCheck( status: ThreadCheckStatus ) {
424+ #if DEBUG
425+ guard self . initialThread != Thread . current
426+ else { return }
427+
428+ let message : String
429+ switch status {
430+ case let . effectCompletion( action) :
431+ message = """
432+ An effect returned from the action " \( debugCaseOutput ( action) ) " completed on the \
433+ wrong thread. Make sure to use " .receive(on:) " on any effects that execute on background \
434+ threads to receive their output on the initial thread.
435+ """
436+
437+ case let . send( action, isFromViewStore: true ) :
438+ message = """
439+ " ViewStore.send( \( debugCaseOutput ( action) ) ) " was called on the wrong thread. Make \
440+ sure that " ViewStore.send " is always called on the initial thread.
441+ """
442+
443+ case let . send( action, isFromViewStore: false ) :
444+ message = """
445+ An effect emitted the action " \( debugCaseOutput ( action) ) " from the wrong thread. Make sure \
446+ to use " .receive(on:) " on any effects that execute on background threads to receive their \
447+ output on the initial thread.
448+ """
449+ }
450+
451+ breakpoint (
452+ """
453+ ---
454+ Warning:
455+
456+ The store was interacted with on a thread that is different from the thread the store was \
457+ created on:
458+
459+ \( message)
460+
461+ Initial thread: \( self . initialThread)
462+ Current thread: \( Thread . current)
463+
464+ The " Store " class is not thread-safe, and so all interactions with an instance of " Store " \
465+ (including all of its scopes and derived view stores) must be done on the same thread.
466+ ---
467+ """
468+ )
469+ #endif
470+ }
443471}
0 commit comments