Skip to content

Commit 215c0f2

Browse files
committed
Add collect argument to dispatch
1 parent 8e64660 commit 215c0f2

File tree

4 files changed

+173
-63
lines changed

4 files changed

+173
-63
lines changed

Sources/RecombinePackage/Store/BaseStore.swift

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,15 @@ public class BaseStore<State: Equatable, RawAction, RefinedAction>: StoreProtoco
123123
_actionsPairedWithState.eraseToAnyPublisher()
124124
}
125125

126-
open func dispatch<S: Sequence>(serially: Bool = false, actions: S) where S.Element == Action {
126+
open func dispatch<S: Sequence>(
127+
serially: Bool = false,
128+
collect: Bool = false,
129+
actions: S
130+
) where S.Element == Action {
127131
let maxPublishers: Subscribers.Demand = serially ? .max(1) : .unlimited
128132
weak var `self` = self
129133

130-
func reduce(actions: Action) -> AnyPublisher<[RefinedAction], Never> {
134+
func recurse(actions: Action) -> AnyPublisher<[RefinedAction], Never> {
131135
switch actions {
132136
case let .raw(actions):
133137
self?._rawActions.send(actions)
@@ -137,20 +141,33 @@ public class BaseStore<State: Equatable, RawAction, RefinedAction>: StoreProtoco
137141
$0.thunk.transform($0.$state.first(), action)
138142
}
139143
}
140-
.flatMap(maxPublishers: maxPublishers, reduce(actions:))
144+
.flatMap(maxPublishers: maxPublishers, recurse(actions:))
141145
.eraseToAnyPublisher()
142146
case let .refined(actions):
143147
return Just(actions).eraseToAnyPublisher()
144148
}
145149
}
146150

147-
actions
148-
.publisher
149-
.flatMap(maxPublishers: maxPublishers, reduce(actions:))
150-
.sink {
151-
self?._preMiddlewareRefinedActions.send($0)
152-
}
153-
.store(in: &cancellables)
151+
let recursed = actions.publisher.flatMap(
152+
maxPublishers: maxPublishers,
153+
recurse(actions:)
154+
)
155+
156+
if collect {
157+
recursed
158+
.collect()
159+
.map { $0.flatMap { $0 } }
160+
.sink {
161+
self?._preMiddlewareRefinedActions.send($0)
162+
}
163+
.store(in: &cancellables)
164+
} else {
165+
recursed
166+
.sink {
167+
self?._preMiddlewareRefinedActions.send($0)
168+
}
169+
.store(in: &cancellables)
170+
}
154171
}
155172

156173
open func injectBypassingMiddleware<S: Sequence>(actions: S) where S.Element == RefinedAction {

Sources/RecombinePackage/Store/StoreProtocol.swift

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public protocol StoreProtocol: ObservableObject, Subscriber {
1414
var underlying: Underlying { get }
1515
var stateLens: (BaseState) -> SubState { get }
1616
var actionPromotion: (SubRefinedAction) -> BaseRefinedAction { get }
17-
func dispatch<S: Sequence>(serially: Bool, actions: S) where S.Element == Action
17+
func dispatch<S: Sequence>(serially: Bool, collect: Bool, actions: S) where S.Element == Action
1818
func eraseToAnyStore() -> AnyStore<BaseState, SubState, RawAction, BaseRefinedAction, SubRefinedAction>
1919
}
2020

@@ -173,9 +173,14 @@ public extension StoreProtocol {
173173
}
174174

175175
public extension StoreProtocol {
176-
func dispatch<S: Sequence>(serially: Bool = false, actions: S) where S.Element == Action {
176+
func dispatch<S: Sequence>(
177+
serially: Bool = false,
178+
collect: Bool = false,
179+
actions: S
180+
) where S.Element == Action {
177181
underlying.dispatch(
178182
serially: serially,
183+
collect: collect,
179184
actions: actions.map {
180185
switch $0 {
181186
case let .refined(actions):
@@ -187,16 +192,28 @@ public extension StoreProtocol {
187192
)
188193
}
189194

190-
func dispatch(serially: Bool = false, actions: Action...) {
191-
dispatch(serially: serially, actions: actions)
195+
func dispatch(
196+
serially: Bool = false,
197+
collect: Bool = false,
198+
actions: Action...
199+
) {
200+
dispatch(serially: serially, collect: collect, actions: actions)
192201
}
193202

194-
func dispatch<S: Sequence>(serially: Bool = false, raw actions: S) where S.Element == RawAction {
195-
dispatch(serially: serially, actions: .raw(.init(actions)))
203+
func dispatch<S: Sequence>(
204+
serially: Bool = false,
205+
collect: Bool = false,
206+
raw actions: S
207+
) where S.Element == RawAction {
208+
dispatch(serially: serially, collect: collect, actions: .raw(.init(actions)))
196209
}
197210

198-
func dispatch(serially: Bool = false, raw actions: RawAction...) {
199-
dispatch(serially: serially, actions: .raw(actions))
211+
func dispatch(
212+
serially: Bool = false,
213+
collect: Bool = false,
214+
raw actions: RawAction...
215+
) {
216+
dispatch(serially: serially, collect: collect, actions: .raw(actions))
200217
}
201218

202219
func dispatch<S: Sequence>(refined actions: S) where S.Element == SubRefinedAction {

Tests/RecombineTests/Extensions.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ extension XCTestCase {
2525
_ store: Store,
2626
dropFirst: Int = 0,
2727
timeout: TimeInterval = 1,
28-
serially: Bool = false,
29-
actions: [ActionStrata<[Store.RawAction], [Store.SubRefinedAction]>],
28+
access: (Store) -> Void,
3029
keyPath: KeyPath<Store.SubState, State>,
3130
value: State
3231
) throws {
@@ -35,12 +34,32 @@ extension XCTestCase {
3534
store,
3635
dropFirst: dropFirst,
3736
timeout: timeout,
38-
access: { $0.dispatch(serially: serially, actions: actions) }
37+
access: access
3938
)?[keyPath: keyPath],
4039
value
4140
)
4241
}
4342

43+
func nextEquals<Store: StoreProtocol, State: Equatable>(
44+
_ store: Store,
45+
dropFirst: Int = 0,
46+
timeout: TimeInterval = 1,
47+
serially: Bool = false,
48+
collect: Bool = false,
49+
actions: [ActionStrata<[Store.RawAction], [Store.SubRefinedAction]>],
50+
keyPath: KeyPath<Store.SubState, State>,
51+
value: State
52+
) throws {
53+
try nextEquals(
54+
store,
55+
dropFirst: dropFirst,
56+
timeout: timeout,
57+
access: { $0.dispatch(serially: serially, collect: collect, actions: actions) },
58+
keyPath: keyPath,
59+
value: value
60+
)
61+
}
62+
4463
func prefix<Store: StoreProtocol>(
4564
_ store: Store,
4665
count: Int,
@@ -57,6 +76,7 @@ extension XCTestCase {
5776
count: Int,
5877
timeout: TimeInterval = 1,
5978
serially: Bool = false,
79+
collect: Bool = false,
6080
actions: [ActionStrata<[Store.RawAction], [Store.SubRefinedAction]>],
6181
keyPath: KeyPath<Store.SubState, State>,
6282
values: [State]
@@ -66,7 +86,7 @@ extension XCTestCase {
6686
store,
6787
count: count,
6888
timeout: timeout,
69-
access: { $0.dispatch(serially: serially, actions: actions) }
89+
access: { $0.dispatch(serially: serially, collect: collect, actions: actions) }
7090
).map { $0[keyPath: keyPath] },
7191
values
7292
)

Tests/RecombineTests/StoreDispatchTests.swift

Lines changed: 97 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,60 +6,75 @@ import XCTest
66
private typealias StoreTestType = BaseStore<TestFakes.IntTest.State, TestFakes.SetAction, TestFakes.SetAction>
77

88
class ObservableStoreDispatchTests: XCTestCase {
9-
fileprivate var store: StoreTestType!
10-
var reducer: MutatingReducer<TestFakes.IntTest.State, TestFakes.SetAction>!
9+
enum RawAction: Equatable {
10+
case addTwice(String)
11+
case addThrice(String)
12+
}
13+
14+
let thunk = Thunk<String, RawAction, String> { _, action -> AnyPublisher
15+
<ActionStrata<[RawAction], [String]>, Never> in
16+
switch action {
17+
case let .addTwice(value):
18+
return Just(value)
19+
.append(
20+
Just(value)
21+
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)
22+
)
23+
.map { .refined($0) }
24+
.eraseToAnyPublisher()
25+
case let .addThrice(value):
26+
return Just(.raw(.addTwice(value)))
27+
.append(
28+
Just(.refined(value))
29+
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)
30+
)
31+
.eraseToAnyPublisher()
32+
}
33+
}
1134

12-
override func setUp() {
13-
super.setUp()
14-
reducer = TestFakes.IntTest.reducer
35+
let reducer: MutatingReducer<String, String> = .init { state, action in
36+
state += action
1537
}
1638

1739
/**
1840
it subscribes to the property we pass in and dispatches any new values
1941
*/
20-
func testLiftingWorksAsExpected() {
21-
let subject = PassthroughSubject<StoreTestType.Action, Never>()
22-
store = BaseStore(state: TestFakes.IntTest.State(), reducer: reducer, middleware: .init(), publishOn: ImmediateScheduler.shared)
23-
let recorder = store.recorder
42+
func testLiftingWorksAsExpected() throws {
43+
let store = BaseStore(
44+
state: "",
45+
reducer: reducer,
46+
thunk: thunk,
47+
publishOn: ImmediateScheduler.shared
48+
)
2449

25-
subject.subscribe(store)
26-
subject.send(.refined(.int(20)))
50+
let subject = PassthroughSubject<ActionStrata<[RawAction], [String]>, Never>()
51+
let rawActionsRecorder = store.rawActions.record()
52+
let refinedActionsRecorder = store.postMiddlewareRefinedActions.record()
53+
54+
try nextEquals(
55+
store,
56+
dropFirst: 2,
57+
access: {
58+
subject.subscribe($0)
59+
subject.send(.raw(.addThrice("1")))
60+
},
61+
keyPath: \.self,
62+
value: "111"
63+
)
64+
65+
let rawExpectation: [RawAction] = [.addThrice("1"), .addTwice("1")]
66+
XCTAssertEqual(
67+
try wait(for: rawActionsRecorder.prefix(2), timeout: 10),
68+
rawExpectation.map { [$0] }
69+
)
2770

2871
XCTAssertEqual(
29-
try wait(for: recorder.next(), timeout: 1).value,
30-
20
72+
try wait(for: refinedActionsRecorder.prefix(3), timeout: 10),
73+
"111".map { [String($0)] }
3174
)
3275
}
3376

3477
func testSerialDispatch() throws {
35-
enum RawAction: Equatable {
36-
case addTwice(String)
37-
case addThrice(String)
38-
}
39-
40-
let thunk = Thunk<String, RawAction, String> { _, action -> AnyPublisher
41-
<ActionStrata<[RawAction], [String]>, Never> in
42-
switch action {
43-
case let .addTwice(value):
44-
return Just(value)
45-
.append(
46-
Just(value)
47-
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)
48-
)
49-
.map { .refined($0) }
50-
.eraseToAnyPublisher()
51-
case let .addThrice(value):
52-
return Just(.raw(.addTwice(value)))
53-
.append(
54-
Just(.refined(value))
55-
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)
56-
)
57-
.eraseToAnyPublisher()
58-
}
59-
}
60-
let reducer: MutatingReducer<String, String> = .init { state, action in
61-
state += action
62-
}
6378
let store = BaseStore(
6479
state: "",
6580
reducer: reducer,
@@ -108,4 +123,45 @@ class ObservableStoreDispatchTests: XCTestCase {
108123
refinedExpectation
109124
)
110125
}
126+
127+
func testSerialDispatchWithCollect() throws {
128+
let store = BaseStore(
129+
state: "",
130+
reducer: reducer,
131+
thunk: thunk,
132+
publishOn: ImmediateScheduler.shared
133+
)
134+
135+
let rawActionsRecorder = store.rawActions.record()
136+
let refinedActionsRecorder = store.postMiddlewareRefinedActions.record()
137+
138+
let value = "5500666221"
139+
140+
try nextEquals(
141+
store,
142+
timeout: 10,
143+
serially: true,
144+
collect: true,
145+
actions: [
146+
.raw(.addTwice("5")),
147+
.refined(["0", "0"]),
148+
.raw(.addThrice("6")),
149+
.raw(.addTwice("2")),
150+
.refined("1"),
151+
],
152+
keyPath: \.self,
153+
value: "5500666221"
154+
)
155+
156+
let rawExpectation: [RawAction] = [.addTwice("5"), .addThrice("6"), .addTwice("6"), .addTwice("2")]
157+
XCTAssertEqual(
158+
try wait(for: rawActionsRecorder.prefix(rawExpectation.count), timeout: 10).flatMap { $0 },
159+
rawExpectation
160+
)
161+
162+
XCTAssertEqual(
163+
try wait(for: refinedActionsRecorder.next(), timeout: 10),
164+
value.map { String($0) }
165+
)
166+
}
111167
}

0 commit comments

Comments
 (0)