Skip to content

Commit 33279f4

Browse files
authored
Update to thread warnings. (#773)
* Update to thread warnings. * fix tests * clean up * rename variable
1 parent 7ede19f commit 33279f4

File tree

3 files changed

+71
-39
lines changed

3 files changed

+71
-39
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

Sources/ComposableArchitecture/ViewStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public final class ViewStore<State, Action>: ObservableObject {
7171
_ store: Store<State, Action>,
7272
removeDuplicates isDuplicate: @escaping (State, State) -> Bool
7373
) {
74-
self._send = store.send
74+
self._send = { store.send($0) }
7575
self._state = CurrentValueSubject(store.state.value)
7676

7777
self.viewCancellable = store.state

Tests/ComposableArchitectureTests/DebugTests.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,18 @@ final class DebugTests: XCTestCase {
112112
}
113113

114114
func testBindingAction() {
115+
struct State {
116+
@BindableState var width = 0
117+
}
118+
115119
var dump = ""
116-
customDump(BindingAction.set(\CGSize.width, 50), to: &dump)
120+
customDump(BindingAction.set(\State.$width, 50), to: &dump)
117121
XCTAssertNoDifference(
118122
dump,
119123
#"""
120124
BindingAction.set(
121-
\CGSize.width,
122-
50.0
125+
WritableKeyPath<State, BindableState<Int>>,
126+
50
123127
)
124128
"""#
125129
)

0 commit comments

Comments
 (0)