Skip to content

Commit

Permalink
Store referencing refactoring (#11)
Browse files Browse the repository at this point in the history
* renamed weak ref store to something more explicit

* test refactoring

* minor tests improvements

* tests fix

* increased timeout to fix test on CI

* added delay to fix test on CI
  • Loading branch information
KazaiMazai authored Mar 31, 2024
1 parent fcb9932 commit a8bb586
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 68 deletions.
4 changes: 2 additions & 2 deletions Sources/Puredux/Store/Core/StoreNodeExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ extension StoreNode where LocalState == State {
}

extension StoreNode {
func store() -> Store<State, Action> {
func weakRefStore() -> Store<State, Action> {
Store(dispatch: { [weak self] in self?.dispatch($0) },
subscribe: { [weak self] in self?.subscribe(observer: $0) }
)
}

func referencedStore() -> Store<State, Action> {
func strongRefStore() -> Store<State, Action> {
Store.referencedStore(dispatch: { [self] in self.dispatch($0) },
subscribe: { [self] in self.subscribe(observer: $0) }
)
Expand Down
6 changes: 3 additions & 3 deletions Sources/Puredux/Store/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public extension Store {
///
///
func scope<LocalState>(toOptional localState: @escaping (State) -> LocalState?) -> Store<LocalState, Action> {
let store = store()
let store = weakRefStore()
return Store<LocalState, Action>(
dispatch: store.dispatch,
subscribe: { localStateObserver in
Expand All @@ -113,7 +113,7 @@ public extension Store {
/// When the result local state is nill, subscribers are not triggered.
///
func scope<LocalState>(to localState: @escaping (State) -> LocalState) -> Store<LocalState, Action> {
let store = store()
let store = weakRefStore()
return Store<LocalState, Action>(
dispatch: store.dispatch,
subscribe: { localStateObserver in
Expand All @@ -126,7 +126,7 @@ public extension Store {
}

extension Store {
func store() -> Store<State, Action> {
func weakRefStore() -> Store<State, Action> {
switch storeType {
case .storeObject(let referencedStore):
return referencedStore.weakRefStore()
Expand Down
8 changes: 4 additions & 4 deletions Sources/Puredux/Store/StoreFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public extension StoreFactory {
/// Store is thread safe. Actions can be dispatched from any thread. Can be subscribed from any thread.
///
func rootStore() -> Store<State, Action> {
rootStoreNode.store()
rootStoreNode.weakRefStore()
}

/// Initializes a new Store with state mapping to local substate.
Expand All @@ -62,7 +62,7 @@ public extension StoreFactory {
/// Store is thread safe. Actions can be dispatched from any thread. Can be subscribed from any thread.
///
func scopeStore<LocalState>(to localState: @escaping (State) -> LocalState) -> Store<LocalState, Action> {
rootStoreNode.store().scope(to: localState)
rootStoreNode.weakRefStore().scope(to: localState)
}

/// Initializes a new Store with state mapping to local substate.
Expand All @@ -75,7 +75,7 @@ public extension StoreFactory {
/// When the result local state is nill, subscribers are not triggered.
///
func scopeStore<LocalState>(toOptional localState: @escaping (State) -> LocalState?) -> Store<LocalState, Action> {
rootStoreNode.store().scope(toOptional: localState)
rootStoreNode.weakRefStore().scope(toOptional: localState)
}

/// Initializes a new child store with initial state
Expand Down Expand Up @@ -118,7 +118,7 @@ public extension StoreFactory {
qos: qos,
reducer: reducer
)
.referencedStore()
.strongRefStore()
}

/// Initializes a new child store with initial state
Expand Down
2 changes: 1 addition & 1 deletion Sources/Puredux/SwiftUI/Store/PublishingStoreObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public extension PublishingStoreObject {
/// PublishingStore is thread safe. Actions can be safely dispatched from any thread.
///
func store() -> PublishingStore<AppState, Action> {
let store = undelyingStore.store()
let store = undelyingStore.weakRefStore()
return PublishingStore(
statePublisher: statePublisher(),
dispatch: { store.dispatch($0) }
Expand Down
4 changes: 2 additions & 2 deletions Sources/Puredux/UIKit/Presentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ public extension Presentable {
presentationQueue: PresentationQueue = .sharedPresentationQueue,
removeStateDuplicates equating: Equating<State>? = nil) {

let weakStore = store.store()
let weakRefStore = store.weakRefStore()
let observer = Observer(
self,
removeStateDuplicates: equating,
observe: { [weak self] state, complete in
presentationQueue.dispatchQueue.async {
let props = props(state, weakStore)
let props = props(state, weakRefStore)

PresentationQueue.main.dispatchQueue.async { [weak self] in
self?.setProps(props)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class StoreNodeChildStoreRefCyclesTests: XCTestCase {
)

weakChildStore = strongChildStore
store = strongChildStore.store()
store = strongChildStore.weakRefStore()
}

XCTAssertNil(weakChildStore)
Expand All @@ -51,7 +51,7 @@ final class StoreNodeChildStoreRefCyclesTests: XCTestCase {
)

weakChildStore = strongChildStore
referencedStore = strongChildStore.referencedStore()
referencedStore = strongChildStore.strongRefStore()
}

XCTAssertNotNil(weakChildStore)
Expand All @@ -71,7 +71,7 @@ final class StoreNodeChildStoreRefCyclesTests: XCTestCase {
)

weakChildStore = strongChildStore
referencedStore = strongChildStore.referencedStore()
referencedStore = strongChildStore.strongRefStore()
}

referencedStore = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,30 @@ import XCTest

final class StoreNodeChildStoreObserverRefCycleTests: XCTestCase {
typealias ParentStore = StoreNode<VoidStore<Action>, TestState, TestState, Action>
typealias ChildStore = StoreNode<ParentStore, ChildTestState, StateComposition, Action>
typealias ChildStore = StoreNode<ParentStore, ReferenceTypeState, ReferenceTypeState, Action>

let timeout: TimeInterval = 3
let rootStore = RootStoreNode<TestState, Action>.initRootStore(
initialState: TestState(currentIndex: 0),
reducer: { state, action in state.reduce(action: action) }
)

func test_WhenStrongRefToStoreObjectAndObserverLive_ThenReferencCycleIsCreated() {
func test_WhenStrongRefToStoreAndObserverLive_ThenReferencCycleIsCreated() {
weak var weakRefObject: ReferenceTypeState?
weak var weakChildStore: ChildStore?

autoreleasepool {
let strongRefObject = ReferenceTypeState()
let strongChildStore = rootStore.createChildStore(
initialState: strongRefObject,
let object = ReferenceTypeState()
let store = rootStore.createChildStore(
initialState: object,
stateMapping: { state, childState in childState },
reducer: { state, action in state.reduce(action) }
)
weakRefObject = strongRefObject

weakRefObject = object
weakChildStore = store

let referencedStore = strongChildStore.referencedStore()
let referencedStore = store.strongRefStore()

let observer = Observer<ReferenceTypeState> { _, complete in
referencedStore.dispatch(UpdateIndex(index: 1))
Expand All @@ -41,60 +44,71 @@ final class StoreNodeChildStoreObserverRefCycleTests: XCTestCase {
}

XCTAssertNotNil(weakRefObject)
XCTAssertNotNil(weakChildStore)
}

func test_WhenStrongRefToStoreObjectAndObserverDead_ThenStoreIsReleased() {
func test_WhenStrongRefToStoreAndObserverDead_ThenStoreIsReleased() {
weak var weakRefObject: ReferenceTypeState?
weak var weakChildStore: ChildStore?

let asyncExpectation = expectation(description: "Observer state handler")

autoreleasepool {
let strongRefObject = ReferenceTypeState()
let strongChildStore = rootStore.createChildStore(
initialState: strongRefObject,
let object = ReferenceTypeState()
let store = rootStore.createChildStore(
initialState: object,
stateMapping: { state, childState in childState },
reducer: { state, action in state.reduce(action) }
)
weakRefObject = strongRefObject

weakRefObject = object
weakChildStore = store

let referencedStore = strongChildStore.referencedStore()
let referencedStore = store.strongRefStore()

let observer = Observer<ReferenceTypeState> { _, complete in
referencedStore.dispatch(UpdateIndex(index: 1))
complete(.dead)
asyncExpectation.fulfill()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
asyncExpectation.fulfill()
}
}

referencedStore.subscribe(observer: observer)
}

waitForExpectations(timeout: timeout) { _ in
XCTAssertNil(weakRefObject)
XCTAssertNil(weakChildStore)
}
}

func test_WhenWeakStoreAndObserverLive_ThenStoreIsReleased() {
weak var weakRefObject: ReferenceTypeState?
weak var weakChildStore: ChildStore?

autoreleasepool {
let strongRefObject = ReferenceTypeState()
let strongChildStore = rootStore.createChildStore(
initialState: strongRefObject,
let object = ReferenceTypeState()
let store = rootStore.createChildStore(
initialState: object,
stateMapping: { state, childState in childState },
reducer: { state, action in state.reduce(action) }
)

weakRefObject = strongRefObject

let store = strongChildStore.store()
weakRefObject = object
weakChildStore = store

let weakRefStore = store.weakRefStore()

let observer = Observer<ReferenceTypeState> { _, complete in
store.dispatch(UpdateIndex(index: 1))
weakRefStore.dispatch(UpdateIndex(index: 1))
complete(.active)
}

store.subscribe(observer: observer)
}

XCTAssertNil(weakChildStore)
XCTAssertNil(weakRefObject)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// File.swift
//
//
//
// Created by Sergey Kazakov on 16.02.2023.
//
Expand All @@ -9,59 +9,59 @@ import XCTest
@testable import Puredux

final class StoreNodeRootStoreRefCyclesTests: XCTestCase {
let timeout: TimeInterval = 3

func test_WhenStore_ThenWeakRefToRootCreated() {

func test_WhenWeakRefStore_ThenWeakRefToRootCreated() {
weak var weakRootStore: RootStoreNode<TestState, Action>?
var store: Store<TestState, Action>?

autoreleasepool {
let strongRootStore = RootStoreNode<TestState, Action>.initRootStore(
let rootStore = RootStoreNode<TestState, Action>.initRootStore(
initialState: TestState(currentIndex: 1)) { state, action in

state.reduce(action: action)
}

weakRootStore = strongRootStore
store = strongRootStore.store()
weakRootStore = rootStore
store = rootStore.weakRefStore()
}

XCTAssertNil(weakRootStore)
}

func test_WhenStoreObject_ThenStrongRefToRootCreated() {
func test_WhenStoreExists_ThenStrongRefToRootCreated() {
weak var weakRootStore: RootStoreNode<TestState, Action>?
var store: Store<TestState, Action>?

autoreleasepool {
let strongRootStore = RootStoreNode<TestState, Action>.initRootStore(
let rootStore = RootStoreNode<TestState, Action>.initRootStore(
initialState: TestState(currentIndex: 1)) { state, action in

state.reduce(action: action)
}

weakRootStore = strongRootStore
store = strongRootStore.referencedStore()
weakRootStore = rootStore
store = rootStore.strongRefStore()
}

XCTAssertNotNil(weakRootStore)
}

func test_WhenStoreObjectReleased_ThenRootStoreIsReleased() {
func test_WhenStoreRemoved_ThenRootStoreIsReleased() {
weak var weakRootStore: RootStoreNode<TestState, Action>?
var store: Store<TestState, Action>?

autoreleasepool {
let strongRootStore = RootStoreNode<TestState, Action>.initRootStore(
let rootStore = RootStoreNode<TestState, Action>.initRootStore(
initialState: TestState(currentIndex: 1)) { state, action in

state.reduce(action: action)
}

weakRootStore = strongRootStore
store = strongRootStore.referencedStore()
weakRootStore = rootStore
store = rootStore.strongRefStore()
}


XCTAssertNotNil(weakRootStore)
store = nil
XCTAssertNil(weakRootStore)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ extension RootStoreSwiftUITests {

func test_WhenActionDispatched_ThenExpectedStateReceived() {
let receivedState = expectation(description: "receivedState")
receivedState.assertForOverFulfill = false
let expectedIndex = 100

var lastReceievedState: Int?

let cancellable = store.statePublisher.sink { state in
lastReceievedState = state.subStateWithIndex.index
receivedState.fulfill()
if lastReceievedState == expectedIndex {
receivedState.fulfill()
}
}

store.dispatch(UpdateIndex(index: expectedIndex))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ extension ViewWithStoreRenderTests {
}
}

waitForExpectations(timeout: max(actionDelay * Double(actionsCount) * 2, 10))
waitForExpectations(timeout: max(actionDelay * Double(actionsCount) * 4, 10))
}

func test_WhenManyActionsDispatchedPropsNotChanged_ThenViewRenderedOnce() {
Expand Down

0 comments on commit a8bb586

Please sign in to comment.