Skip to content

Commit bd49bf4

Browse files
Rewrite scope strategy (#43)
1 parent 3670253 commit bd49bf4

File tree

3 files changed

+111
-80
lines changed

3 files changed

+111
-80
lines changed

Sources/Store.swift

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
1212
didSet {
1313
queue.sync {
1414
if state != oldValue {
15-
stateCurrentValueSubject.send(state)
16-
statePassthroughSubject.send(state)
15+
stateSubject.send(state)
1716
}
1817
}
1918
}
@@ -22,15 +21,12 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
2221

2322
public init(_ state: StoreState,
2423
dispatcher: Dispatcher,
25-
storeController: StoreController,
26-
defaultPublisherMode: DefaultPublisherMode = .currentValue) {
24+
storeController: StoreController) {
2725
self.initialState = state
2826
self.dispatcher = dispatcher
29-
self.stateCurrentValueSubject = .init(state)
30-
self.statePassthroughSubject = .init()
27+
self.stateSubject = .init(state)
3128
self.storeController = storeController
3229
self.state = state
33-
self.defaultPublisherMode = defaultPublisherMode
3430
}
3531

3632
/**
@@ -57,8 +53,7 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
5753
}
5854

5955
public func replayOnce() {
60-
stateCurrentValueSubject.send(state)
61-
statePassthroughSubject.send(state)
56+
stateSubject.send(state)
6257

6358
dispatcher.stateWasReplayed(state: state)
6459
}
@@ -71,56 +66,85 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
7166
publisher.receive(subscriber: subscriber)
7267
}
7368

74-
public var publisher: StorePublisher {
75-
switch defaultPublisherMode {
76-
case .passthrough:
77-
return passthroughPublisher
78-
79-
case .currentValue:
80-
return currentValuePublisher
81-
}
82-
}
83-
84-
public var passthroughPublisher: StorePublisher {
85-
.init(subject: statePassthroughSubject)
86-
}
87-
88-
public var currentValuePublisher: StorePublisher {
89-
.init(subject: stateCurrentValueSubject)
69+
public var publisher: Publishers.StoreStatePublisher<StoreState> {
70+
.init(upstream: stateSubject)
9071
}
9172

9273
/// Scope a task from the state and receive only new updated since subscription.
93-
public func scope<T: Taskable & Equatable>(_ transform: @escaping (StoreState) -> T) -> AnyPublisher<T, Failure> {
94-
passthroughPublisher
95-
.map(transform)
96-
.removeDuplicates()
97-
.eraseToAnyPublisher()
74+
public func scope<T: Taskable>(_ transform: @escaping (StoreState) -> T) -> Publishers.StoreScopePublisher<T> {
75+
Publishers.StoreScopePublisher(upstream: stateSubject.map(transform),
76+
initialValue: transform(state))
9877
}
9978

100-
private var stateCurrentValueSubject: CurrentValueSubject<StoreState, Never>
101-
private var statePassthroughSubject: PassthroughSubject<StoreState, Never>
79+
private var stateSubject: CurrentValueSubject<StoreState, Never>
10280
private let queue = DispatchQueue(label: "atomic state")
103-
private let defaultPublisherMode: DefaultPublisherMode
10481
}
10582

106-
public extension Store {
107-
enum DefaultPublisherMode {
108-
case passthrough
109-
case currentValue
83+
public extension Publishers {
84+
class StoreStatePublisher<StoreState: State>: Publisher {
85+
public typealias Upstream = any Subject<StoreState, Never>
86+
public typealias Output = StoreState
87+
public typealias Failure = Never
88+
89+
private let upstream: Upstream
90+
91+
internal init(upstream: Upstream) {
92+
self.upstream = upstream
93+
}
94+
95+
public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
96+
upstream.subscribe(subscriber)
97+
}
11098
}
11199

112-
class StorePublisher: Publisher {
113-
public typealias Output = StoreState
100+
struct StoreScopePublisher<StoreTask: Taskable>: Publisher {
101+
public typealias Upstream = any Publisher<StoreTask, Never>
102+
public typealias Output = StoreTask
114103
public typealias Failure = Never
115104

116-
private var subject: any Subject<StoreState, Never>
105+
private let upstream: Upstream
106+
private let initialValue: StoreTask
117107

118-
internal init(subject: any Subject<StoreState, Never>) {
119-
self.subject = subject
108+
internal init(upstream: Upstream, initialValue: StoreTask) {
109+
self.upstream = upstream
110+
self.initialValue = initialValue
120111
}
121112

122113
public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
123-
subject.subscribe(subscriber)
114+
upstream.subscribe(Inner(downstream: subscriber, initialValue: initialValue))
115+
}
116+
}
117+
}
118+
119+
extension Publishers.StoreScopePublisher {
120+
private class Inner<Downstream: Subscriber>: Subscriber
121+
where Downstream.Input == Output, Downstream.Failure == Never, Output == StoreTask {
122+
public typealias Input = Output
123+
public typealias Failure = Never
124+
125+
let combineIdentifier = CombineIdentifier()
126+
private let downstream: Downstream
127+
private var lastValue: StoreTask
128+
129+
fileprivate init(downstream: Downstream, initialValue: StoreTask) {
130+
self.downstream = downstream
131+
self.lastValue = initialValue
132+
}
133+
134+
func receive(subscription: Subscription) {
135+
downstream.receive(subscription: subscription)
136+
}
137+
138+
func receive(_ input: Output) -> Subscribers.Demand {
139+
if input == lastValue {
140+
return .none
141+
}
142+
self.lastValue = input
143+
return downstream.receive(input)
144+
}
145+
146+
func receive(completion: Subscribers.Completion<Failure>) {
147+
downstream.receive(completion: completion)
124148
}
125149
}
126150
}

Tests/ReducerTests.swift

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ final class ReducerTests: XCTestCase {
6969
XCTAssertEqual(store.state, initialState)
7070
}
7171

72-
func test_subscribe_state_changes_with_initial_value() {
72+
func test_subscribe_state_changes() {
7373
var cancellables = Set<AnyCancellable>()
7474
let dispatcher = Dispatcher()
7575
let initialState = TestStateWithOneTask()
76-
let store = Store<TestStateWithOneTask, TestStoreController>(initialState, dispatcher: dispatcher, storeController: TestStoreController(), defaultPublisherMode: .currentValue)
76+
let store = Store<TestStateWithOneTask, TestStoreController>(initialState, dispatcher: dispatcher, storeController: TestStoreController())
7777
let expectation1 = XCTestExpectation(description: "Subscription Emits 1")
7878
let expectation2 = XCTestExpectation(description: "Subscription Emits 2")
7979

@@ -97,38 +97,4 @@ final class ReducerTests: XCTestCase {
9797
dispatcher.dispatch(TestAction(counter: 2))
9898
wait(for: [expectation1, expectation2], timeout: 5.0)
9999
}
100-
101-
func test_subscribe_state_changes_without_initial_value() {
102-
var cancellables = Set<AnyCancellable>()
103-
let dispatcher = Dispatcher()
104-
let initialState = TestStateWithOneTask()
105-
let store = Store<TestStateWithOneTask, TestStoreController>(initialState, dispatcher: dispatcher, storeController: TestStoreController(), defaultPublisherMode: .passthrough)
106-
let expectation = XCTestExpectation(description: "Subscription Emits")
107-
108-
store
109-
.reducerGroup()
110-
.store(in: &cancellables)
111-
112-
dispatcher.dispatch(TestAction(counter: 1))
113-
114-
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
115-
// Only gets the action with counter == 2.
116-
store
117-
.map(\.counter)
118-
.sink { counter in
119-
if counter == 1 {
120-
XCTFail("counter == 1 should not be emmited because this is a stateless subscription")
121-
}
122-
if counter == 2 {
123-
expectation.fulfill()
124-
}
125-
}
126-
.store(in: &cancellables)
127-
128-
// Send action with counter == 2, this action should be caught by the two subscriptions
129-
dispatcher.dispatch(TestAction(counter: 2))
130-
}
131-
132-
wait(for: [expectation], timeout: 5.0)
133-
}
134100
}

Tests/StoreTests.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Combine
33
import XCTest
44

55
class StoreTests: XCTestCase {
6-
func test_scope() {
6+
func test_scope_with_initial_state() {
77
var cancellables = Set<AnyCancellable>()
88
let expectation = XCTestExpectation(description: "Scope usage check")
99
expectation.expectedFulfillmentCount = 2
@@ -37,4 +37,45 @@ class StoreTests: XCTestCase {
3737

3838
XCTAssertTrue(counterValue == 2)
3939
}
40+
41+
func test_scope_with_initial_change_after_subscriptions() {
42+
var cancellables = Set<AnyCancellable>()
43+
let expectation = XCTestExpectation(description: "Scope usage check")
44+
expectation.expectedFulfillmentCount = 2
45+
let dispatcher = Dispatcher()
46+
let initialState = TestStateWithTwoTasks()
47+
let store = Store<TestStateWithTwoTasks, TestStoreController>(initialState, dispatcher: dispatcher, storeController: TestStoreController())
48+
49+
var counterValue = 0
50+
// SCOPING....
51+
store
52+
.scope { $0.testTask1 }
53+
.sink { _ in
54+
expectation.fulfill()
55+
counterValue += 1
56+
}
57+
.store(in: &cancellables)
58+
59+
Thread.sleep(forTimeInterval: 1)
60+
61+
// THIS NOT PASS: change task2
62+
store.state = TestStateWithTwoTasks(testTask1: store.state.testTask1,
63+
testTask2: .success(10))
64+
65+
// THIS PASSES: change task1
66+
store.state = TestStateWithTwoTasks(testTask1: .success(6),
67+
testTask2: store.state.testTask2)
68+
69+
// THIS NOT PASS: change task2
70+
store.state = TestStateWithTwoTasks(testTask1: store.state.testTask1,
71+
testTask2: .success(2))
72+
73+
// THIS PASSES: change task1
74+
store.state = TestStateWithTwoTasks(testTask1: .success(7),
75+
testTask2: store.state.testTask2)
76+
77+
wait(for: [expectation], timeout: 5.0)
78+
79+
XCTAssertTrue(counterValue == 2)
80+
}
4081
}

0 commit comments

Comments
 (0)