Skip to content

Commit

Permalink
Prepare Stores to emit or not the initial value
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianvarela committed May 31, 2024
1 parent 540ea93 commit e1b196e
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 66 deletions.
67 changes: 24 additions & 43 deletions Sources/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,31 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
public let dispatcher: Dispatcher
public var storeController: StoreController
public var state: StoreState {
get {
_state
}
set {
didSet {
queue.sync {
if newValue != _state {
_state = newValue
stateCurrentValueSubject.send(state)
statePassthroughSubject.send(state)
if state != oldValue {
if emitsInitialValue {
stateCurrentValueSubject.send(state)
} else {
statePassthroughSubject.send(state)
}
}
}
}
}
public var initialState: StoreState {
_initialState
}
public var initialState: StoreState

public init(_ state: StoreState,
dispatcher: Dispatcher,
storeController: StoreController) {
self._initialState = state
self._state = state
storeController: StoreController,
emitsInitialValue: Bool = true) {
self.initialState = state
self.dispatcher = dispatcher
self.stateCurrentValueSubject = .init(state)
self.statePassthroughSubject = .init()
self.storeController = storeController
self.state = _initialState
self.emitsInitialValue = emitsInitialValue
self.state = state
}

/**
Expand All @@ -61,15 +59,12 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
}
}

/// Exposes a passthrough publisher for the Store.
/// Doesn’t have an initial value or a buffer of the most recently-published state.
public lazy var passthrough: PassthroughStorePublisher = {
.init(statePassthroughSubject: statePassthroughSubject)
}()

public func replayOnce() {
stateCurrentValueSubject.send(state)
statePassthroughSubject.send(state)
if emitsInitialValue {
stateCurrentValueSubject.send(state)
} else {
statePassthroughSubject.send(state)

Check warning on line 66 in Sources/Store.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Store.swift#L66

Added line #L66 was not covered by tests
}

dispatcher.stateWasReplayed(state: state)
}
Expand All @@ -79,29 +74,15 @@ public class Store<StoreState: State, StoreController: Cancellable>: Publisher {
}

public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
stateCurrentValueSubject.subscribe(subscriber)
if emitsInitialValue {
stateCurrentValueSubject.subscribe(subscriber)
} else {
statePassthroughSubject.subscribe(subscriber)
}
}

private var stateCurrentValueSubject: CurrentValueSubject<StoreState, Never>
private var statePassthroughSubject: PassthroughSubject<StoreState, Never>
private let queue = DispatchQueue(label: "atomic state")
private var _initialState: StoreState
private var _state: StoreState
}

public extension Store {
class PassthroughStorePublisher: Publisher {
public typealias Output = StoreState
public typealias Failure = Never

private var statePassthroughSubject: PassthroughSubject<StoreState, Never>

internal init(statePassthroughSubject: PassthroughSubject<StoreState, Never>) {
self.statePassthroughSubject = statePassthroughSubject
}

public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
statePassthroughSubject.subscribe(subscriber)
}
}
private let emitsInitialValue: Bool
}
28 changes: 5 additions & 23 deletions Tests/ReducerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,12 @@ final class ReducerTests: XCTestCase {
wait(for: [expectation1, expectation2], timeout: 5.0)
}

func test_subscribe_unbuffered_state_changes() {
func test_subscribe_state_changes_without_initial_value() {
var cancellables = Set<AnyCancellable>()
let dispatcher = Dispatcher()
let initialState = TestState()
let store = Store<TestState, TestStoreController>(initialState, dispatcher: dispatcher, storeController: TestStoreController())
let expectation1 = XCTestExpectation(description: "Subscription Emits 1")
let expectation2 = XCTestExpectation(description: "Subscription Emits 2")
expectation2.expectedFulfillmentCount = 2
let store = Store<TestState, TestStoreController>(initialState, dispatcher: dispatcher, storeController: TestStoreController(), emitsInitialValue: false)
let expectation = XCTestExpectation(description: "Subscription Emits")

store
.reducerGroup()
Expand All @@ -114,31 +112,15 @@ final class ReducerTests: XCTestCase {
dispatcher.dispatch(TestAction(counter: 1))

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Subscribe delayed and get action with counter == 1 inmediatly because the normal
// behaviour of the store maintains a buffer of the most recently published state.
store
.map(\.counter)
.sink { counter in
if counter == 1 {
expectation1.fulfill()
}
if counter == 2 {
expectation2.fulfill()
}
}
.store(in: &cancellables)

// Subscribe through passthrough publisher and bypass the buffer from the store.
// Only gets the action with counter == 2.
store
.passthrough
.map(\.counter)
.sink { counter in
if counter == 1 {
XCTFail("counter == 1 should not be emmited because this is a stateless subscription")
}
if counter == 2 {
expectation2.fulfill()
expectation.fulfill()
}
}
.store(in: &cancellables)
Expand All @@ -147,7 +129,7 @@ final class ReducerTests: XCTestCase {
dispatcher.dispatch(TestAction(counter: 2))
}

wait(for: [expectation1, expectation2], timeout: 5.0)
wait(for: [expectation], timeout: 5.0)
}

func test_scope() {
Expand Down

0 comments on commit e1b196e

Please sign in to comment.