Skip to content

Commit 9836fec

Browse files
mbrandonwstephencelis
authored andcommitted
Buffer actions when sent recursively (#287)
* wip * wip * explicit test store * test * test * clean up Co-authored-by: Stephen Celis <[email protected]>
1 parent 8e49f03 commit 9836fec

File tree

2 files changed

+55
-26
lines changed

2 files changed

+55
-26
lines changed

Sources/ComposableArchitecture/Store.swift

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public final class Store<State, Action> {
1111
private var isSending = false
1212
private let reducer: (inout State, Action) -> Effect<Action, Never>
1313
private var synchronousActionsToSend: [Action] = []
14+
private var bufferedActions: [Action] = []
1415

1516
/// Initializes a store from an initial state, a reducer, and an environment.
1617
///
@@ -131,32 +132,18 @@ public final class Store<State, Action> {
131132
}
132133

133134
func send(_ action: Action) {
134-
self.synchronousActionsToSend.append(action)
135-
136-
while !self.synchronousActionsToSend.isEmpty {
137-
let action = self.synchronousActionsToSend.removeFirst()
138-
139-
if self.isSending {
140-
assertionFailure(
141-
"""
142-
The store was sent the action \(debugCaseOutput(action)) while it was already
143-
processing another action.
144-
145-
This can happen for a few reasons:
146-
147-
* The store was sent an action recursively. This can occur when you run an effect \
148-
directly in the reducer, rather than returning it from the reducer. Check the stack (⌘7) \
149-
to find frames corresponding to one of your reducers. That code should be refactored to \
150-
not invoke the effect directly.
151-
152-
* The store has been sent actions from multiple threads. The `send` method is not \
153-
thread-safe, and should only ever be used from a single thread (typically the main \
154-
thread). Instead of calling `send` from multiple threads you should use effects to \
155-
process expensive computations on background threads so that it can be fed back into the \
156-
store.
157-
"""
158-
)
159-
}
135+
if !self.isSending {
136+
self.synchronousActionsToSend.append(action)
137+
} else {
138+
self.bufferedActions.append(action)
139+
return
140+
}
141+
142+
while !self.synchronousActionsToSend.isEmpty || !self.bufferedActions.isEmpty {
143+
let action = !self.synchronousActionsToSend.isEmpty
144+
? self.synchronousActionsToSend.removeFirst()
145+
: self.bufferedActions.removeFirst()
146+
160147
self.isSending = true
161148
let effect = self.reducer(&self.state, action)
162149
self.isSending = false

Tests/ComposableArchitectureTests/StoreTests.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,46 @@ final class StoreTests: XCTestCase {
296296
XCTAssertEqual(vs.state, 3)
297297
}
298298
}
299+
300+
func testActionQueuing() {
301+
let subject = PassthroughSubject<Void, Never>()
302+
303+
enum Action: Equatable {
304+
case incrementTapped
305+
case `init`
306+
case doIncrement
307+
}
308+
309+
let store = TestStore(
310+
initialState: 0,
311+
reducer: Reducer<Int, Action, Void> { state, action, _ in
312+
switch action {
313+
case .incrementTapped:
314+
subject.send()
315+
return .none
316+
317+
case .`init`:
318+
return subject.map { .doIncrement }.eraseToEffect()
319+
320+
case .doIncrement:
321+
state += 1
322+
return .none
323+
}
324+
},
325+
environment: ()
326+
)
327+
328+
store.assert(
329+
.send(.`init`),
330+
.send(.incrementTapped),
331+
.receive(.doIncrement) {
332+
$0 = 1
333+
},
334+
.send(.incrementTapped),
335+
.receive(.doIncrement) {
336+
$0 = 2
337+
},
338+
.do { subject.send(completion: .finished) }
339+
)
340+
}
299341
}

0 commit comments

Comments
 (0)