diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 9b99bb0..c01dd69 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ C84645911EB775AF0008AD87 /* SystemError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84645901EB775AF0008AD87 /* SystemError.swift */; }; C84645931EB78D320008AD87 /* UILabel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84645921EB78D320008AD87 /* UILabel+Extensions.swift */; }; C84893732190FAE900561487 /* AsyncSynchronized.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84893692190FAE900561487 /* AsyncSynchronized.swift */; }; - C84893742190FAE900561487 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84893722190FAE900561487 /* Identifiable.swift */; }; C8576BE9214EE1F0000A62AE /* RxFeedback.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8576BE5214EE1F0000A62AE /* RxFeedback.plist */; }; C8576BEA214EE1F0000A62AE /* Feedbacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8576BE6214EE1F0000A62AE /* Feedbacks.swift */; }; C8576BEB214EE1F1000A62AE /* Deprecations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8576BE7214EE1F0000A62AE /* Deprecations.swift */; }; @@ -216,7 +215,6 @@ C84645901EB775AF0008AD87 /* SystemError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemError.swift; sourceTree = ""; }; C84645921EB78D320008AD87 /* UILabel+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+Extensions.swift"; sourceTree = ""; }; C84893692190FAE900561487 /* AsyncSynchronized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncSynchronized.swift; sourceTree = ""; }; - C84893722190FAE900561487 /* Identifiable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; }; C8576BE5214EE1F0000A62AE /* RxFeedback.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = RxFeedback.plist; sourceTree = ""; }; C8576BE6214EE1F0000A62AE /* Feedbacks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feedbacks.swift; sourceTree = ""; }; C8576BE7214EE1F0000A62AE /* Deprecations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Deprecations.swift; sourceTree = ""; }; @@ -320,7 +318,6 @@ isa = PBXGroup; children = ( C84893692190FAE900561487 /* AsyncSynchronized.swift */, - C84893722190FAE900561487 /* Identifiable.swift */, C8576BE5214EE1F0000A62AE /* RxFeedback.plist */, C8576BE6214EE1F0000A62AE /* Feedbacks.swift */, C8576BE7214EE1F0000A62AE /* Deprecations.swift */, @@ -642,7 +639,6 @@ C84893732190FAE900561487 /* AsyncSynchronized.swift in Sources */, C8576BEB214EE1F1000A62AE /* Deprecations.swift in Sources */, C8576BEA214EE1F0000A62AE /* Feedbacks.swift in Sources */, - C84893742190FAE900561487 /* Identifiable.swift in Sources */, C8576BEC214EE1F1000A62AE /* ObservableType+RxFeedback.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/Examples/GithubPaginatedSearch.swift b/Examples/Examples/GithubPaginatedSearch.swift index d455ea2..b880aa9 100644 --- a/Examples/Examples/GithubPaginatedSearch.swift +++ b/Examples/Examples/GithubPaginatedSearch.swift @@ -144,7 +144,7 @@ class GithubPaginatedSearchViewController: UIViewController { // UI, user feedback bindUI, // NoUI, automatic feedback - react(query: { $0.loadNextPage }, effects: { resource in + react(request: { $0.loadNextPage }, effects: { resource in return URLSession.shared.loadRepositories(resource: resource) .asSignal(onErrorJustReturn: .failure(.offline)) .map(Mutation.response) diff --git a/Examples/Examples/PlayCatch.swift b/Examples/Examples/PlayCatch.swift index 5a129fe..def4242 100644 --- a/Examples/Examples/PlayCatch.swift +++ b/Examples/Examples/PlayCatch.swift @@ -55,13 +55,13 @@ class PlayCatchViewController: UIViewController { case .throwToHuman: return .humanHasIt } - }, + }, scheduler: MainScheduler.instance, scheduledFeedback: // UI is human feedback bindUI, // NoUI, machine feedback - react(query: { $0.machinePitching }, effects: { () -> Observable in + react(request: { $0.machinePitching }, effects: { (_) -> Observable in return Observable .timer(1.0, scheduler: MainScheduler.instance) .map { _ in Mutation.throwToHuman } @@ -100,7 +100,9 @@ extension State { } } - var machinePitching: ()? { - return self == .machineHasIt ? () : nil + var machinePitching: PitchRequest? { + return self == .machineHasIt ? PitchRequest() : nil } } + +struct PitchRequest: Equatable {} diff --git a/Examples/Examples/Todo+Feedback.swift b/Examples/Examples/Todo+Feedback.swift index 9295877..a52eddb 100644 --- a/Examples/Examples/Todo+Feedback.swift +++ b/Examples/Examples/Todo+Feedback.swift @@ -18,7 +18,7 @@ extension Todo { ui: @escaping Feedback, synchronizeTask: @escaping (Task) -> Single) -> Driver { - let synchronizeFeedback: Feedback = react(query: { $0.tasksToSynchronize }) { task -> Signal in + let synchronizeFeedback: Feedback = react(requests: { $0.tasksToSynchronize }) { task -> Signal in return synchronizeTask(task.value) .map { Todo.Mutation.synchronizationChanged(task, $0) } .asSignal(onErrorRecover: { error in Signal.just(.synchronizationChanged(task, .failed(error))) diff --git a/README.md b/README.md index a9669c3..c91e744 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Observable.system( // UI is human feedback bindUI, // NoUI, machine feedback - react(query: { $0.machinePitching }, effects: { () -> Observable in + react(request: { $0.machinePitching }, effects: { () -> Observable in return Observable .timer(1.0, scheduler: MainScheduler.instance) .map { _ in Mutation.throwToHuman } @@ -114,7 +114,7 @@ Driver.system( // UI, user feedback bindUI, // NoUI, automatic feedback - react(query: { $0.loadNextPage }, effects: { resource in + react(request: { $0.loadNextPage }, effects: { resource in return URLSession.shared.loadRepositories(resource: resource) .asDriver(onErrorJustReturn: .failure(.offline)) .map(Mutation.response) diff --git a/RxFeedback.xcodeproj/project.pbxproj b/RxFeedback.xcodeproj/project.pbxproj index 68a894e..c8513f6 100644 --- a/RxFeedback.xcodeproj/project.pbxproj +++ b/RxFeedback.xcodeproj/project.pbxproj @@ -19,9 +19,7 @@ C8996C7A1F40C5B6004E5D83 /* Deprecations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8996C791F40C5B6004E5D83 /* Deprecations.swift */; }; C8996C7C1F40D82A004E5D83 /* String+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8996C7B1F40D82A004E5D83 /* String+Test.swift */; }; C89B96251EB6964200BDAB24 /* Feedbacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89B96241EB6964200BDAB24 /* Feedbacks.swift */; }; - C89F167C218F7148009BA457 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89F167B218F7148009BA457 /* Identifiable.swift */; }; C8DF7346218F389200945A7C /* AsyncSynchronized.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DF7345218F389200945A7C /* AsyncSynchronized.swift */; }; - E11E0E1E1F766F370031D189 /* ReactNonEquatableLoopsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11E0E0A1F766C750031D189 /* ReactNonEquatableLoopsTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,9 +58,7 @@ C8996C791F40C5B6004E5D83 /* Deprecations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Deprecations.swift; sourceTree = ""; }; C8996C7B1F40D82A004E5D83 /* String+Test.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Test.swift"; sourceTree = ""; }; C89B96241EB6964200BDAB24 /* Feedbacks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feedbacks.swift; sourceTree = ""; }; - C89F167B218F7148009BA457 /* Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; }; C8DF7345218F389200945A7C /* AsyncSynchronized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSynchronized.swift; sourceTree = ""; }; - E11E0E0A1F766C750031D189 /* ReactNonEquatableLoopsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactNonEquatableLoopsTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -125,7 +121,6 @@ C89B96241EB6964200BDAB24 /* Feedbacks.swift */, C8996C791F40C5B6004E5D83 /* Deprecations.swift */, C8DF7345218F389200945A7C /* AsyncSynchronized.swift */, - C89F167B218F7148009BA457 /* Identifiable.swift */, ); name = RxFeedback; path = Sources/RxFeedback; @@ -194,7 +189,6 @@ C8996C6C1F40A7C1004E5D83 /* RxFeedbackObservableTests.swift */, C8996C691F40A7A0004E5D83 /* RxFeedbackDriverTests.swift */, C8996C7B1F40D82A004E5D83 /* String+Test.swift */, - E11E0E0A1F766C750031D189 /* ReactNonEquatableLoopsTests.swift */, C83596431FB22EE900DC7B8F /* ReactEquatableLoopsTests.swift */, C83596451FB2312D00DC7B8F /* ReactHashableLoopsTests.swift */, C83596471FB24CAF00DC7B8F /* RxTest.swift */, @@ -316,7 +310,6 @@ C89B96251EB6964200BDAB24 /* Feedbacks.swift in Sources */, C8DF7346218F389200945A7C /* AsyncSynchronized.swift in Sources */, C8996C7A1F40C5B6004E5D83 /* Deprecations.swift in Sources */, - C89F167C218F7148009BA457 /* Identifiable.swift in Sources */, C834A8691EB6795F00E6B15E /* ObservableType+RxFeedback.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -325,7 +318,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E11E0E1E1F766F370031D189 /* ReactNonEquatableLoopsTests.swift in Sources */, C83596441FB22EE900DC7B8F /* ReactEquatableLoopsTests.swift in Sources */, C83596481FB24CAF00DC7B8F /* RxTest.swift in Sources */, C8996C6B1F40A7A0004E5D83 /* RxFeedbackDriverTests.swift in Sources */, diff --git a/Sources/RxFeedback/Deprecations.swift b/Sources/RxFeedback/Deprecations.swift index d945f23..5c81f24 100644 --- a/Sources/RxFeedback/Deprecations.swift +++ b/Sources/RxFeedback/Deprecations.swift @@ -5,393 +5,3 @@ // Created by Krunoslav Zaher on 8/13/17. // Copyright © 2017 Krunoslav Zaher. All rights reserved. // - -import RxCocoa -import RxSwift - -/** - Control feedback loop that tries to immediatelly perform the latest required effect. - - * State: State type of the system. - * Control: Subset of state used to control the feedback loop. - - When query result exists (not `nil`), feedback loop is active and it performs effects. - - When query result is `nil`, feedback loops doesn't perform any effect. - - - parameter query: State type of the system - - parameter effects: Control state which is subset of state. - - returns: Feedback loop performing the effects. - */ -@available(*, deprecated, message: "Renamed to version that takes `ObservableSchedulerContext` as argument.", renamed: "react(query:effects:)") -public func react( - query: @escaping (State) -> Control?, - effects: @escaping (Control) -> Observable -) -> (Observable) -> Observable { - return { state in - let context = ObservableSchedulerContext(source: state, scheduler: CurrentThreadScheduler.instance) - return react(query: query, effects: effects)(context) - } -} - -/** - Control feedback loop that tries to immediatelly perform the latest required effect. - - * State: State type of the system. - * Control: Subset of state used to control the feedback loop. - - When query result exists (not `nil`), feedback loop is active and it performs effects. - - When query result is `nil`, feedback loops doesn't perform any effect. - - - parameter query: State type of the system - - parameter effects: Control state which is subset of state. - - returns: Feedback loop performing the effects. - */ -@available(*, deprecated, message: "Renamed to version that takes `ObservableSchedulerContext` as argument.", renamed: "react(query:effects:)") -public func react( - query: @escaping (State) -> Control?, - effects: @escaping (Control) -> Observable -) -> (Observable) -> Observable { - return { state in - let context = ObservableSchedulerContext(source: state, scheduler: CurrentThreadScheduler.instance) - return react(query: query, effects: effects)(context) - } -} - -extension ObservableType where E == Any { - /** - Simulation of a discrete system (finite-state machine) with feedback loops. - Interpretations: - - [system with feedback loops](https://en.wikipedia.org/wiki/Control_theory) - - [fixpoint solver](https://en.wikipedia.org/wiki/Fixed_point) - - [local equilibrium point calculator](https://en.wikipedia.org/wiki/Mechanical_equilibrium) - - .... - - System simulation will be started upon subscription and stopped after subscription is disposed. - - System state is represented as a `State` parameter. - Mutations are represented by `Mutation` parameter. - - - parameter initialState: Initial state of the system. - - parameter accumulator: Calculates new system state from existing state and a transition mutation (system integrator, reducer). - - parameter feedback: Feedback loops that produce mutations depending on current system state. - - returns: Current state of the system. - */ - @available(*, deprecated, message: "Renamed to version that takes `ObservableSchedulerContext` as argument.", renamed: "system(initialState:reduce:scheduler:scheduledFeedback:)") - public static func system( - initialState: State, - reduce: @escaping (State, Mutation) -> State, - scheduler: ImmediateSchedulerType, - feedback: [(Observable) -> Observable] - ) -> Observable { - let observableFeedbacks: [(ObservableSchedulerContext) -> Observable] = feedback.map { feedback in - return { sourceSchedulerContext in - feedback(sourceSchedulerContext.source) - } - } - - return Observable.system( - initialState: initialState, - reduce: reduce, - scheduler: scheduler, - scheduledFeedback: observableFeedbacks - ) - } - - @available(*, deprecated, message: "Renamed to version that takes `ObservableSchedulerContext` as argument.", renamed: "system(initialState:reduce:scheduler:scheduledFeedback:)") - public static func system( - initialState: State, - reduce: @escaping (State, Mutation) -> State, - scheduler: ImmediateSchedulerType, - feedback: (Observable) -> Observable... - ) -> Observable { - return system(initialState: initialState, reduce: reduce, scheduler: scheduler, feedback: feedback) - } -} - -/** - Control feedback loop that tries to immediatelly perform the latest required effect. - - * State: State type of the system. - * Control: Subset of state used to control the feedback loop. - - When query result exists (not `nil`), feedback loop is active and it performs effects. - - When query result is `nil`, feedback loops doesn't perform any effect. - - - parameter query: State type of the system - - parameter effects: Control state which is subset of state. - - returns: Feedback loop performing the effects. - */ -@available(*, deprecated, message: "Please use version that uses feedback with this signature `Driver -> Signal`") -public func react( - query: @escaping (State) -> Control?, - effects: @escaping (Control) -> Driver -) -> (Driver) -> Driver { - return { state in - state.map(query) - .distinctUntilChanged { $0 == $1 } - .flatMapLatest { (control: Control?) -> Driver in - guard let control = control else { - return Driver.empty() - } - - return effects(control) - .enqueue() - } - } -} - -/** - Control feedback loop that tries to immediatelly perform the latest required effect. - - * State: State type of the system. - * Control: Subset of state used to control the feedback loop. - - When query result exists (not `nil`), feedback loop is active and it performs effects. - - When query result is `nil`, feedback loops doesn't perform any effect. - - - parameter query: State type of the system - - parameter effects: Control state which is subset of state. - - returns: Feedback loop performing the effects. - */ -@available(*, deprecated, message: "Please use version that uses feedback with this signature `Driver -> Signal`") -public func react( - query: @escaping (State) -> Control?, - effects: @escaping (Control) -> Driver -) -> (Driver) -> Driver { - return { state in - state.map(query) - .distinctUntilChanged { $0 != nil } - .flatMapLatest { (control: Control?) -> Driver in - guard let control = control else { - return Driver.empty() - } - - return effects(control) - .enqueue() - } - } -} - -/** - Control feedback loop that tries to immediatelly perform the latest required effect. - - * State: State type of the system. - * Control: Subset of state used to control the feedback loop. - - When query result exists (not `nil`), feedback loop is active and it performs effects. - - When query result is `nil`, feedback loops doesn't perform any effect. - - - parameter query: State type of the system - - parameter effects: Control state which is subset of state. - - returns: Feedback loop performing the effects. - */ -@available(*, deprecated, message: "Please use version that uses feedback with this signature `Driver -> Signal`") -public func react( - query: @escaping (State) -> Set, - effects: @escaping (Control) -> Driver -) -> (Driver) -> Driver { - return { state in - let query = state.map(query) - - let newQueries = Driver.zip(query, query.startWith(Set())) { $0.subtracting($1) } - - return newQueries.flatMap { controls in - Driver.merge( - controls.map { control -> Driver in - query.filter { !$0.contains(control) } - .map { _ in Driver.empty() } - .startWith(effects(control).enqueue()) - .switchLatest() - } - ) - } - } -} - -extension SharedSequence where SharingStrategy == DriverSharingStrategy { - fileprivate func enqueue() -> Driver { - return asObservable() - // observe on is here because results should be cancelable - .observeOn(S.scheduler.async) - // subscribe on is here because side-effects also need to be cancelable - // (smooths out any glitches caused by start-cancel immediatelly) - .subscribeOn(S.scheduler.async) - .asDriver(onErrorDriveWith: Driver.empty()) - } -} - -extension SharedSequenceConvertibleType where E == Any, SharingStrategy == DriverSharingStrategy { - /// Feedback loop - @available(*, deprecated, message: "Please use Feedback") - public typealias FeedbackLoop = (Driver) -> Driver - - /** - System simulation will be started upon subscription and stopped after subscription is disposed. - - System state is represented as a `State` parameter. - Mutations are represented by `Mutation` parameter. - - - parameter initialState: Initial state of the system. - - parameter accumulator: Calculates new system state from existing state and a transition mutation (system integrator, reducer). - - parameter feedback: Feedback loops that produce mutations depending on current system state. - - returns: Current state of the system. - */ - @available(*, deprecated, message: "Please use version that uses feedbacks with this signature `Driver -> Signal`") - public static func system( - initialState: State, - reduce: @escaping (State, Mutation) -> State, - feedback: [FeedbackLoop] - ) -> Driver { - let observableFeedbacks: [(ObservableSchedulerContext) -> Observable] = feedback.map { feedback in - return { sharedSequence in - feedback(sharedSequence.source.asDriver(onErrorDriveWith: Driver.empty())) - .asObservable() - } - } - - return Observable.system( - initialState: initialState, - reduce: reduce, - scheduler: SharingStrategy.scheduler, - scheduledFeedback: observableFeedbacks - ) - .asDriver(onErrorDriveWith: .empty()) - } - - @available(*, deprecated, message: "Please use version that uses feedback with this signature `Driver -> Signal`") - public static func system( - initialState: State, - reduce: @escaping (State, Mutation) -> State, - feedback: FeedbackLoop... - ) -> Driver { - return system(initialState: initialState, reduce: reduce, feedback: feedback) - } -} - -public extension Bindings { - @available(*, deprecated, message: "Please use Bindings(subscriptions:mutations:) instead.") - convenience init(subscriptions: [Disposable], events: [Observable]) { - self.init(subscriptions: subscriptions, mutations: events) - } - - @available(*, deprecated, message: "Please use Bindings(subscriptions:mutations:) instead.") - convenience init(subscriptions: [Disposable], events: [Signal]) { - self.init(subscriptions: subscriptions, mutations: events) - } -} - -@available(*, deprecated, message: "Please use free members from RxFeedback module (`RxFeedback.Bindings`, `RxFeedback.bind`, ...).") -public struct UI { - /** - Contains subscriptions and mutations. - - `subscriptions` map a system state to UI presentation. - - `mutations` map mutations from UI to mutations of a given system. - */ - public class Bindings: Disposable { - fileprivate let subscriptions: [Disposable] - fileprivate let mutations: [Observable] - - /** - - parameters: - - subscriptions: mappings of a system state to UI presentation. - - mutations: mappings of mutations from UI to mutations of a given system - */ - public init(subscriptions: [Disposable], mutations: [Observable]) { - self.subscriptions = subscriptions - self.mutations = mutations - } - - /** - - parameters: - - subscriptions: mappings of a system state to UI presentation. - - mutations: mappings of mutations from UI to mutations of a given system - */ - public init(subscriptions: [Disposable], mutations: [Driver]) { - self.subscriptions = subscriptions - self.mutations = mutations.map { $0.asObservable() } - } - - public func dispose() { - for subscription in subscriptions { - subscription.dispose() - } - } - } - - /** - Bi-directional binding of a system State to UI and UI into Mutations. - */ - public static func bind(_ bindings: @escaping (ObservableSchedulerContext) -> (Bindings)) -> (ObservableSchedulerContext) -> Observable { - return { (state: ObservableSchedulerContext) -> Observable in - Observable.using( - { () -> Bindings in - bindings(state) - }, observableFactory: { (bindings: Bindings) -> Observable in - Observable - .merge(bindings.mutations) - .enqueue(state.scheduler) - } - ) - } - } - - /** - Bi-directional binding of a system State to UI and UI into Mutations, - Strongify owner. - */ - public static func bind(_ owner: WeakOwner, _ bindings: @escaping (WeakOwner, ObservableSchedulerContext) -> (Bindings)) - -> (ObservableSchedulerContext) -> Observable where WeakOwner: AnyObject { - return bind(bindingsStrongify(owner, bindings)) - } - - /** - Bi-directional binding of a system State to UI and UI into Mutations. - */ - public static func bind(_ bindings: @escaping (Driver) -> (Bindings)) -> (Driver) -> Driver { - return { (state: Driver) -> Driver in - Observable.using( - { () -> Bindings in - bindings(state) - }, observableFactory: { (bindings: Bindings) -> Observable in - Observable.merge(bindings.mutations) - } - ).asDriver(onErrorDriveWith: Driver.empty()) - .enqueue() - } - } - - /** - Bi-directional binding of a system State to UI and UI into Mutations, - Strongify owner. - */ - public static func bind(_ owner: WeakOwner, _ bindings: @escaping (WeakOwner, Driver) -> (Bindings)) - -> (Driver) -> Driver where WeakOwner: AnyObject { - return bind(bindingsStrongify(owner, bindings)) - } - - private static func bindingsStrongify(_ owner: WeakOwner, _ bindings: @escaping (WeakOwner, O) -> (Bindings)) - -> (O) -> (Bindings) where WeakOwner: AnyObject { - return { [weak owner] state -> Bindings in - guard let strongOwner = owner else { - return Bindings(subscriptions: [], mutations: [Observable]()) - } - return bindings(strongOwner, state) - } - } -} - -extension Observable { - fileprivate func enqueue(_ scheduler: ImmediateSchedulerType) -> Observable { - return self - // observe on is here because results should be cancelable - .observeOn(scheduler) - // subscribe on is here because side-effects also need to be cancelable - // (smooths out any glitches caused by start-cancel immediatelly) - .subscribeOn(scheduler) - } -} diff --git a/Sources/RxFeedback/Feedbacks.swift b/Sources/RxFeedback/Feedbacks.swift index fadfc59..353d9e5 100644 --- a/Sources/RxFeedback/Feedbacks.swift +++ b/Sources/RxFeedback/Feedbacks.swift @@ -12,281 +12,131 @@ import RxSwift /** * State: State type of the system. - * Query: Subset of state used to control the feedback loop. + * Request: Subset of state used to control the feedback loop. - When `query` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. - In case new `query` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. + When `request` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. + In case new `request` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. - When `query` returns `nil`, feedback loops doesn't perform any effect. + When `request` returns `nil`, feedback loops doesn't perform any effect. - - parameter query: Part of state that controls feedback loop. - - parameter areEqual: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query result. + - parameter request: Part of state that controls feedback loop. + - parameter effects: Chooses which effects to perform for certain request result. - returns: Feedback loop performing the effects. */ -public func react( - query: @escaping (State) -> Query?, - areEqual: @escaping (Query, Query) -> Bool, - effects: @escaping (Query) -> Observable +public func react( + request: @escaping (State) -> Request?, + effects: @escaping (Request) -> Observable ) -> (ObservableSchedulerContext) -> Observable { - return { state in - state.map(query) - .distinctUntilChanged { lhs, rhs in - switch (lhs, rhs) { - case (.none, .none): return true - case (.none, .some): return false - case (.some, .none): return false - case (.some(let lhs), .some(let rhs)): return areEqual(lhs, rhs) - } - } - .flatMapLatest { (control: Query?) -> Observable in - guard let control = control else { - return Observable.empty() - } - - return effects(control) - .enqueue(state.scheduler) - } - } -} - -/** - * State: State type of the system. - * Query: Subset of state used to control the feedback loop. - - When `query` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. - In case new `query` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. - - When `query` returns `nil`, feedback loops doesn't perform any effect. - - - parameter query: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query result. - - returns: Feedback loop performing the effects. - */ -public func react( - query: @escaping (State) -> Query?, - effects: @escaping (Query) -> Observable -) -> (ObservableSchedulerContext) -> Observable { - return react(query: query, areEqual: { $0 == $1 }, effects: effects) -} - -/** - * State: State type of the system. - * Query: Subset of state used to control the feedback loop. - - When `query` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. - In case new `query` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. - - When `query` returns `nil`, feedback loops doesn't perform any effect. - - - parameter query: Part of state that controls feedback loop. - - parameter areEqual: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query result. - - returns: Feedback loop performing the effects. - */ -public func react( - query: @escaping (State) -> Query?, - areEqual: @escaping (Query, Query) -> Bool, - effects: @escaping (Query) -> Signal -) -> (Driver) -> Signal { - return { state in - let observableSchedulerContext = ObservableSchedulerContext( - source: state.asObservable(), - scheduler: Signal.SharingStrategy.scheduler.async - ) - return react(query: query, areEqual: areEqual, effects: { effects($0).asObservable() })(observableSchedulerContext) - .asSignal(onErrorSignalWith: .empty()) - } + return react( + requests: { request($0).map { value in [ConstHashable(value: value): value] } ?? [:] }, + effects: { initialValue, _ in + return effects(initialValue) + } + ) } /** * State: State type of the system. - * Query: Subset of state used to control the feedback loop. + * Request: Subset of state used to control the feedback loop. - When `query` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. - In case new `query` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. + When `request` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. + In case new `request` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. - When `query` returns `nil`, feedback loops doesn't perform any effect. + When `request` returns `nil`, feedback loops doesn't perform any effect. - - parameter query: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query result. + - parameter request: Part of state that controls feedback loop. + - parameter effects: Chooses which effects to perform for certain request result. - returns: Feedback loop performing the effects. */ -public func react( - query: @escaping (State) -> Query?, - effects: @escaping (Query) -> Signal +public func react( + request: @escaping (State) -> Request?, + effects: @escaping (Request) -> Signal ) -> (Driver) -> Signal { return { state in let observableSchedulerContext = ObservableSchedulerContext( source: state.asObservable(), scheduler: Signal.SharingStrategy.scheduler.async ) - return react(query: query, effects: { effects($0).asObservable() })(observableSchedulerContext) + return react(request: request, effects: { effects($0).asObservable() })(observableSchedulerContext) .asSignal(onErrorSignalWith: .empty()) } } /** * State: State type of the system. - * Query: Subset of state used to control the feedback loop. + * Request: Subset of state used to control the feedback loop. - When `query` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. - In case new `query` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. + When `request` returns some set of values, each value is being passed into `effects` lambda to decide which effects should be performed. - When `query` returns `nil`, feedback loops doesn't perform any effect. + * Effects are not interrupted for elements in the new `request` that were present in the `old` request. + * Effects are cancelled for elements present in `old` request but not in `new` request. + * In case new elements are present in `new` request (and not in `old` request) they are being passed to the `effects` lambda and resulting effects are being performed. - - parameter query: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query result. + - parameter requests: Part of state that controls feedback loop. + - parameter effects: Chooses which effects to perform for certain request element. - returns: Feedback loop performing the effects. */ -public func react( - query: @escaping (State) -> Query?, - effects: @escaping (Query) -> Observable +public func react( + requests: @escaping (State) -> Set, + effects: @escaping (Request) -> Observable ) -> (ObservableSchedulerContext) -> Observable { - return { state in - state.map(query) - .distinctUntilChanged { $0 != nil } - .flatMapLatest { (control: Query?) -> Observable in - guard let control = control else { - return Observable.empty() - } - - return effects(control) - .enqueue(state.scheduler) - } - } + return react( + requests: { Dictionary(requests($0).map { ($0, $0) }, uniquingKeysWith: { first, _ in first }) }, + effects: { initialValue, _ in + return effects(initialValue) + }) } /** * State: State type of the system. - * Query: Subset of state used to control the feedback loop. + * Request: Subset of state used to control the feedback loop. - When `query` returns a value, that value is being passed into `effects` lambda to decide which effects should be performed. - In case new `query` is different from the previous one, new effects are calculated by using `effects` lambda and then performed. + When `request` returns some set of values, each value is being passed into `effects` lambda to decide which effects should be performed. - When `query` returns `nil`, feedback loops doesn't perform any effect. + * Effects are not interrupted for elements in the new `request` that were present in the `old` request. + * Effects are cancelled for elements present in `old` request but not in `new` request. + * In case new elements are present in `new` request (and not in `old` request) they are being passed to the `effects` lambda and resulting effects are being performed. - - parameter query: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query result. + - parameter requests: Part of state that controls feedback loop. + - parameter effects: Chooses which effects to perform for certain request element. - returns: Feedback loop performing the effects. */ -public func react( - query: @escaping (State) -> Query?, - effects: @escaping (Query) -> Signal -) -> (Driver) -> Signal { - return { state in - let observableSchedulerContext = ObservableSchedulerContext( - source: state.asObservable(), - scheduler: Signal.SharingStrategy.scheduler.async - ) - return react(query: query, effects: { effects($0).asObservable() })(observableSchedulerContext) - .asSignal(onErrorSignalWith: .empty()) - } -} - -/** - * State: State type of the system. - * Query: Subset of state used to control the feedback loop. - - When `query` returns some set of values, each value is being passed into `effects` lambda to decide which effects should be performed. - - * Effects are not interrupted for elements in the new `query` that were present in the `old` query. - * Effects are cancelled for elements present in `old` query but not in `new` query. - * In case new elements are present in `new` query (and not in `old` query) they are being passed to the `effects` lambda and resulting effects are being performed. - - - parameter query: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query element. - - returns: Feedback loop performing the effects. - */ -public func react( - query: @escaping (State) -> Set, - effects: @escaping (Query) -> Observable -) -> (ObservableSchedulerContext) -> Observable { - return { state in - let query = state.map(query) - .share(replay: 1) - - let newQueries = Observable.zip(query, query.startWith(Set())) { $0.subtracting($1) } - let asyncScheduler = state.scheduler.async - - return newQueries.flatMap { controls in - Observable.merge( - controls.map { control -> Observable in - effects(control) - .enqueue(state.scheduler) - .takeUntilWithCompletedAsync(query.filter { !$0.contains(control) }, scheduler: asyncScheduler) - } - ) - } - } -} - -extension ObservableType { - // This is important to avoid reentrancy issues. Completed mutation is only used for cleanup - fileprivate func takeUntilWithCompletedAsync(_ other: Observable, scheduler: ImmediateSchedulerType) -> Observable { - // this little piggy will delay completed mutation - let completeAsSoonAsPossible = Observable.empty().observeOn(scheduler) - return other - .take(1) - .map { _ in completeAsSoonAsPossible } - // this little piggy will ensure self is being run first - .startWith(asObservable()) - // this little piggy will ensure that new mutations are being blocked immediatelly - .switchLatest() - } -} - -/** - * State: State type of the system. - * Query: Subset of state used to control the feedback loop. - - When `query` returns some set of values, each value is being passed into `effects` lambda to decide which effects should be performed. - - * Effects are not interrupted for elements in the new `query` that were present in the `old` query. - * Effects are cancelled for elements present in `old` query but not in `new` query. - * In case new elements are present in `new` query (and not in `old` query) they are being passed to the `effects` lambda and resulting effects are being performed. - - - parameter query: Part of state that controls feedback loop. - - parameter effects: Chooses which effects to perform for certain query element. - - returns: Feedback loop performing the effects. - */ -public func react( - query: @escaping (State) -> Set, - effects: @escaping (Query) -> Signal +public func react( + requests: @escaping (State) -> Set, + effects: @escaping (Request) -> Signal ) -> (Driver) -> Signal { return { (state: Driver) -> Signal in let observableSchedulerContext = ObservableSchedulerContext( source: state.asObservable(), scheduler: Signal.SharingStrategy.scheduler.async ) - return react(query: query, effects: { effects($0).asObservable() })(observableSchedulerContext) + return react(requests: requests, effects: { effects($0).asObservable() })(observableSchedulerContext) .asSignal(onErrorSignalWith: .empty()) } } /// This is defined outside of `react` because Swift compiler generates an `error` :(. -fileprivate class ChildLifetimeTracking where Child: Equatable { +fileprivate class RequestLifetimeTracking { class LifetimeToken {} let state = AsyncSynchronized( ( isDisposed: false, - lifetimeByIdentifier: [Child.Identity: ChildLifetime]() + lifetimeByIdentifier: [RequestID: RequestLifetime]() ) ) - typealias ChildLifetime = ( - identifier: Child.Identity, + typealias RequestLifetime = ( subscription: Disposable, lifetimeIdentifier: LifetimeToken, - stateBehavior: BehaviorSubject + latestRequest: BehaviorSubject ) - let effects: (_ initial: Child, _ state: Observable) -> Observable + let effects: (_ initial: Request, _ state: Observable) -> Observable let scheduler: ImmediateSchedulerType let observer: AnyObserver init( - effects: @escaping (_ initial: Child, _ state: Observable) -> Observable, + effects: @escaping (_ initial: Request, _ state: Observable) -> Observable, scheduler: ImmediateSchedulerType, observer: AnyObserver ) { @@ -295,32 +145,29 @@ fileprivate class ChildLifetimeTracking where Chi self.observer = observer } - func forwardChildState(_ childStates: [Child]) { + func forwardRequests(_ requests: [RequestID: Request]) { self.state.async { state in guard !state.isDisposed else { return } var lifetimeToUnsubscribeByIdentifier = state.lifetimeByIdentifier - for childState in childStates { - if let childLifetime = state.lifetimeByIdentifier[childState.identifier] { - lifetimeToUnsubscribeByIdentifier.removeValue(forKey: childState.identifier) - guard (try? childLifetime.stateBehavior.value()) != childState else { - continue - } - childLifetime.stateBehavior.onNext(childState) + for (requestID, request) in requests { + if let requestLifetime = state.lifetimeByIdentifier[requestID] { + lifetimeToUnsubscribeByIdentifier.removeValue(forKey: requestID) + guard (try? requestLifetime.latestRequest.value()) != request else { continue } + requestLifetime.latestRequest.onNext(request) } else { let subscription = SingleAssignmentDisposable() - let childStateSubject = BehaviorSubject(value: childState) + let latestRequestSubject = BehaviorSubject(value: request) let lifetime = LifetimeToken() - state.lifetimeByIdentifier[childState.identifier] = ( - identifier: childState.identifier, + state.lifetimeByIdentifier[requestID] = ( subscription: subscription, lifetimeIdentifier: lifetime, - stateBehavior: childStateSubject + latestRequest: latestRequestSubject ) - let childSubscription = self.effects(childState, childStateSubject.asObservable()) + let requestsSubscription = self.effects(request, latestRequestSubject.asObservable()) .observeOn(self.scheduler) .subscribe { event in self.state.async { state in - guard state.lifetimeByIdentifier[childState.identifier]?.lifetimeIdentifier === lifetime else { return } + guard state.lifetimeByIdentifier[requestID]?.lifetimeIdentifier === lifetime else { return } guard !state.isDisposed else { return } switch event { case .next(let mutation): @@ -333,7 +180,7 @@ fileprivate class ChildLifetimeTracking where Chi } } - subscription.setDisposable(childSubscription) + subscription.setDisposable(requestsSubscription) } } @@ -360,43 +207,36 @@ enum DisposeState: Int32 { /** * State: State type of the system. - * Child: Subset of state used to control the feedback loop. + * Request: Subset of state used to control the feedback loop. - For every uniquely identifiable child value `effects` closure is invoked with the initial value of child state and future values corresponding to the same identifier. + For every uniquely identifiable request `effects` closure is invoked with the initial value of the request and future requests corresponding to the same identifier. - Subsequent equal values of child state are not emitted. + Subsequent equal values of request are not emitted from the effects state parameter. - - parameter query: Selects child states. - - parameter effects: Effects to perform for each unique identifier. - - parameter initial: Initial child state. - - parameter state: Changes of child state. + - parameter requests: Selects requests. + - parameter effects: Effects to perform per request identifier. + - parameter initial: Initial request. + - parameter state: Latest request state. - returns: Feedback loop performing the effects. */ -public func react( - childQuery: @escaping (State) -> [Child], - effects: @escaping (_ initial: Child, _ state: Observable) -> Observable -) -> (ObservableSchedulerContext) -> Observable where Child: Identifiable, Child: Equatable { +public func react( + requests: @escaping (State) -> [RequestID: Request], + effects: @escaping (_ initial: Request, _ state: Observable) -> Observable +) -> (ObservableSchedulerContext) -> Observable { return { stateContext in Observable.create { observer in - // This additional check is needed because `state.dispose()` is async. - var isDisposed = AtomicInt() - isDisposed.initialize(DisposeState.subscribed.rawValue) - - let state = ChildLifetimeTracking( + let state = RequestLifetimeTracking( effects: effects, scheduler: stateContext.scheduler, - observer: AnyObserver { event in - guard isDisposed.load() == DisposeState.subscribed.rawValue else { return } - observer.on(event) - } + observer: observer ) let subscription = stateContext.source - .map(childQuery) + .map(requests) .subscribe { event in switch event { - case .next(let childStates): - state.forwardChildState(childStates) + case .next(let requests): + state.forwardRequests(requests) case .error(let error): observer.on(.error(error)) case .completed: @@ -405,7 +245,6 @@ public func react( } return Disposables.create { - isDisposed.fetchOr(DisposeState.disposed.rawValue) state.dispose() subscription.dispose() } @@ -415,29 +254,29 @@ public func react( /** * State: State type of the system. - * Child: Subset of state used to control the feedback loop. + * Request: Subset of state used to control the feedback loop. - For every uniquely identifiable child value `effects` closure is invoked with the initial value of child state and future values corresponding to the same identifier. + For every uniquely identifiable request `effects` closure is invoked with the initial value of the request and future requests corresponding to the same identifier. - Subsequent equal values of child state are not emitted. + Subsequent equal values of request are not emitted from the effects state parameter. - - parameter query: Selects child states. - - parameter effects: Effects to perform for each unique identifier. - - parameter initial: Initial child state. - - parameter state: Changes of child state. + - parameter requests: Selects requests. + - parameter effects: Effects to perform per request identifier. + - parameter initial: Initial request. + - parameter state: Latest request state. - returns: Feedback loop performing the effects. */ -public func react( - childQuery: @escaping (State) -> [Child], - effects: @escaping (_ initial: Child, _ state: Driver) -> Signal -) -> (Driver) -> Signal where Child: Identifiable, Child: Equatable { +public func react( + requests: @escaping (State) -> [RequestID: Request], + effects: @escaping (_ initial: Request, _ state: Driver) -> Signal +) -> (Driver) -> Signal { return { state in let observableSchedulerContext = ObservableSchedulerContext( source: state.asObservable(), scheduler: Signal.SharingStrategy.scheduler.async ) return react( - childQuery: childQuery, + requests: requests, effects: { initial, state in effects( initial, @@ -558,3 +397,14 @@ private func bindingsStrongify(_ owner: WeakOwner, _ bin return bindings(strongOwner, state) } } + +/// `Hashable` wrapper for `Equatable` value that returns const `hashValue`. +/// +/// This looks like a performance issue, but it is ok when there is a single value present. Used in a `react` feedback loop. +fileprivate struct ConstHashable: Hashable { + var value: Value + + var hashValue: Int { + return 0 + } +} diff --git a/Sources/RxFeedback/Identifiable.swift b/Sources/RxFeedback/Identifiable.swift deleted file mode 100644 index db40524..0000000 --- a/Sources/RxFeedback/Identifiable.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Identifiable.swift -// RxFeedback -// -// Created by Krunoslav Zaher on 11/4/18. -// Copyright © 2018 Krunoslav Zaher. All rights reserved. -// - -public protocol Identifiable { - associatedtype Identity: Hashable - - var identifier: Identity { get } -} - diff --git a/Tests/RxFeedbackTests/ReactEquatableLoopsTests.swift b/Tests/RxFeedbackTests/ReactEquatableLoopsTests.swift index 93c41f0..5ffb547 100644 --- a/Tests/RxFeedbackTests/ReactEquatableLoopsTests.swift +++ b/Tests/RxFeedbackTests/ReactEquatableLoopsTests.swift @@ -21,11 +21,11 @@ extension ReactEquatableLoopsTests { func testIntialNilQueryDoesNotProduceEffects() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> String? = { _ in + let request: (String) -> String? = { _ in return nil } let effects: (String) -> Observable = { .just($0 + "_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) + let feedback: (ObservableSchedulerContext) -> Observable = react(request: request, effects: effects) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -47,7 +47,7 @@ extension ReactEquatableLoopsTests { func testNotNilAfterIntialNilDoesProduceEffects() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> String? = { state in + let request: (String) -> String? = { state in if state == "initial+" { return "I" } else { @@ -55,7 +55,7 @@ extension ReactEquatableLoopsTests { } } let effects: (String) -> Observable = { .just($0 + "_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) + let feedback: (ObservableSchedulerContext) -> Observable = react(request: request, effects: effects) let mutations = PublishSubject() let system = Observable.system( initialState: "initial", @@ -74,18 +74,18 @@ extension ReactEquatableLoopsTests { XCTAssertEqual(results.events, [ next(201, "initial"), next(211, "initial+"), - next(213, "initial+I_a"), + next(212, "initial+I_a"), ]) } func testSecondConsecutiveEqualQueryDoesNotProduceEffects() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> String? = { _ in return "Same" } + let request: (String) -> String? = { _ in return "Same" } let effects: (String) -> Observable = { _ in return .just("_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) + let feedback: (ObservableSchedulerContext) -> Observable = react(request: request, effects: effects) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -101,21 +101,21 @@ extension ReactEquatableLoopsTests { // Test XCTAssertEqual(results.events, [ next(201, "initial"), - next(204, "initial_a") + next(203, "initial_a") ]) } func testImmediateEffectsHaveTheSameOrderAsTheyArePassedToSystem() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query1: (String) -> String? = { state in + let request1: (String) -> String? = { state in if state == "initial" { return "_I" } else { return nil } } - let query2: (String) -> String? = { state in + let request2: (String) -> String? = { state in if state == "initial_I_a" { return "_IA" } else { @@ -129,9 +129,9 @@ extension ReactEquatableLoopsTests { return .just($0 + "_b") } let feedback1: (ObservableSchedulerContext) -> Observable - feedback1 = react(query: query1, effects: effects1) + feedback1 = react(request: request1, effects: effects1) let feedback2: (ObservableSchedulerContext) -> Observable - feedback2 = react(query: query2, effects: effects2) + feedback2 = react(request: request2, effects: effects2) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -147,8 +147,8 @@ extension ReactEquatableLoopsTests { // Test XCTAssertEqual(results.events, [ next(201, "initial"), - next(204, "initial_I_a"), - next(206, "initial_I_a_IA_b") + next(203, "initial_I_a"), + next(204, "initial_I_a_IA_b") ]) } @@ -156,14 +156,14 @@ extension ReactEquatableLoopsTests { // Prepare let scheduler = TestScheduler(initialClock: 0) let notImmediateEffect = PublishSubject() - let query1: (String) -> String? = { state in + let request1: (String) -> String? = { state in if state == "initial" { return "_I" } else { return nil } } - let query2: (String) -> String? = { state in + let request2: (String) -> String? = { state in if state == "initial" { return "_I" } else { @@ -179,9 +179,9 @@ extension ReactEquatableLoopsTests { return .just($0 + "_b") } let feedback1: (ObservableSchedulerContext) -> Observable - feedback1 = react(query: query1, effects: effects1) + feedback1 = react(request: request1, effects: effects1) let feedback2: (ObservableSchedulerContext) -> Observable - feedback2 = react(query: query2, effects: effects2) + feedback2 = react(request: request2, effects: effects2) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -198,7 +198,7 @@ extension ReactEquatableLoopsTests { // Test XCTAssertEqual(results.events, [ next(201, "initial"), - next(204, "initial_I_b") + next(203, "initial_I_b") ]) XCTAssertTrue(isEffects1Called) } diff --git a/Tests/RxFeedbackTests/ReactHashableLoopsTests.swift b/Tests/RxFeedbackTests/ReactHashableLoopsTests.swift index 7e8ebd9..ed7cffa 100644 --- a/Tests/RxFeedbackTests/ReactHashableLoopsTests.swift +++ b/Tests/RxFeedbackTests/ReactHashableLoopsTests.swift @@ -21,11 +21,11 @@ extension ReactHashableLoopsTests { func testEmptyQueryDoesNotProduceEffects() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> Set = { _ in + let requests: (String) -> Set = { _ in return Set() } let effects: (String) -> Observable = { .just($0 + "_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) + let feedback: (ObservableSchedulerContext) -> Observable = react(requests: requests, effects: effects) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -47,7 +47,7 @@ extension ReactHashableLoopsTests { func testNonEmptyAfterEmptyDoesProduceEffects() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> Set = { state in + let requests: (String) -> Set = { state in if state == "initial+" { return Set(["I"]) } else { @@ -55,7 +55,7 @@ extension ReactHashableLoopsTests { } } let effects: (String) -> Observable = { .just($0 + "_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) + let feedback: (ObservableSchedulerContext) -> Observable = react(requests: requests, effects: effects) let mutations = PublishSubject() let system = Observable.system( initialState: "initial", @@ -74,18 +74,18 @@ extension ReactHashableLoopsTests { XCTAssertEqual(results.events, [ next(201, "initial"), next(211, "initial+"), - next(213, "initial+I_a"), + next(212, "initial+I_a"), ]) } func testEqualQueryDoesNotProduceEffects() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> Set = { _ in return Set(["Same"]) } + let requests: (String) -> Set = { _ in return Set(["Same"]) } let effects: (String) -> Observable = { _ in return .just("_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) + let feedback: (ObservableSchedulerContext) -> Observable = react(requests: requests, effects: effects) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -101,21 +101,21 @@ extension ReactHashableLoopsTests { // Test XCTAssertEqual(results.events, [ next(201, "initial"), - next(204, "initial_a") + next(203, "initial_a") ]) } func testImmediateEffectsHaveTheSameOrderAsTheyArePassedToSystem() { // Prepare let scheduler = TestScheduler(initialClock: 0) - let query1: (String) -> Set = { state in + let requests1: (String) -> Set = { state in if state == "initial" { return Set(["_I"]) } else { return Set() } } - let query2: (String) -> Set = { state in + let requests2: (String) -> Set = { state in if state == "initial_I_a" { return Set(["_IA"]) } else { @@ -129,9 +129,9 @@ extension ReactHashableLoopsTests { return .just($0 + "_b") } let feedback1: (ObservableSchedulerContext) -> Observable - feedback1 = react(query: query1, effects: effects1) + feedback1 = react(requests: requests1, effects: effects1) let feedback2: (ObservableSchedulerContext) -> Observable - feedback2 = react(query: query2, effects: effects2) + feedback2 = react(requests: requests2, effects: effects2) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -147,8 +147,8 @@ extension ReactHashableLoopsTests { // Test XCTAssertEqual(results.events, [ next(201, "initial"), - next(204, "initial_I_a"), - next(206, "initial_I_a_IA_b") + next(203, "initial_I_a"), + next(204, "initial_I_a_IA_b") ]) } @@ -156,14 +156,14 @@ extension ReactHashableLoopsTests { // Prepare let scheduler = TestScheduler(initialClock: 0) let notImmediateEffect = PublishSubject() - let query1: (String) -> Set = { state in + let requests1: (String) -> Set = { state in if state == "initial" { return Set(["_I"]) } else { return Set() } } - let query2: (String) -> Set = { state in + let requests2: (String) -> Set = { state in if state == "initial" { return Set(["_I"]) } else { @@ -179,9 +179,9 @@ extension ReactHashableLoopsTests { return .just($0 + "_b") } let feedback1: (ObservableSchedulerContext) -> Observable - feedback1 = react(query: query1, effects: effects1) + feedback1 = react(requests: requests1, effects: effects1) let feedback2: (ObservableSchedulerContext) -> Observable - feedback2 = react(query: query2, effects: effects2) + feedback2 = react(requests: requests2, effects: effects2) let system = Observable.system( initialState: "initial", reduce: { oldState, append in @@ -198,7 +198,7 @@ extension ReactHashableLoopsTests { // Test XCTAssertEqual(results.events, [ next(201, "initial"), - next(204, "initial_I_b") + next(203, "initial_I_b") ]) XCTAssertTrue(isEffects1Called) } @@ -207,7 +207,7 @@ extension ReactHashableLoopsTests { // Prepare let scheduler = TestScheduler(initialClock: 0) let initiator = PublishSubject() - let query: (String) -> Set = { state in + let requests: (String) -> Set = { state in if state == "initial" { return Set(["_I", "_I2", "_I3", "_I4"]) } else { @@ -215,21 +215,21 @@ extension ReactHashableLoopsTests { } } var isEffects1Called = false - let effects: (String) -> Observable = { query in - if query == "_I" { - return Observable.just(query + "_done") + let effects: (String) -> Observable = { request in + if request == "_I" { + return Observable.just(request + "_done") .delay(20.0, scheduler: scheduler) - } else if query == "_I2" { + } else if request == "_I2" { isEffects1Called = true - return Observable.just(query + "_done") + return Observable.just(request + "_done") .delay(30.0, scheduler: scheduler) - } else if query == "_I3" { + } else if request == "_I3" { isEffects1Called = true - return Observable.just(query + "_done") + return Observable.just(request + "_done") .delay(30.0, scheduler: scheduler) - } else if query == "_I4" { + } else if request == "_I4" { isEffects1Called = true - return Observable.just(query + "_done") + return Observable.just(request + "_done") .delay(30.0, scheduler: scheduler) } else { fatalError() @@ -241,7 +241,7 @@ extension ReactHashableLoopsTests { return oldState + append }, scheduler: scheduler, - scheduledFeedback: react(query: query, effects: effects), + scheduledFeedback: react(requests: requests, effects: effects), { _ in initiator.asObservable() } ) @@ -276,7 +276,7 @@ extension ReactHashableLoopsTests { next(201, ""), next(211, ""), next(216, "initial"), - next(238, "initial_I_done"), + next(237, "initial_I_done"), ]) XCTAssertTrue(isEffects1Called) } @@ -285,7 +285,7 @@ extension ReactHashableLoopsTests { // Prepare let scheduler = TestScheduler(initialClock: 0) let initiator = PublishSubject() - let query: (String) -> Set = { state in + let requests: (String) -> Set = { state in if state == "initial" { return Set(["_I", "_I2"]) } else if state == "initial_I_done" { @@ -311,7 +311,7 @@ extension ReactHashableLoopsTests { return oldState + append }, scheduler: scheduler, - scheduledFeedback: react(query: query, effects: effects), + scheduledFeedback: react(requests: requests, effects: effects), { _ in initiator.asObservable() } ) @@ -341,8 +341,8 @@ extension ReactHashableLoopsTests { next(201, ""), next(211, ""), next(216, "initial"), - next(238, "initial_I_done"), - next(248, "initial_I_done_I2_done"), + next(237, "initial_I_done"), + next(247, "initial_I_done_I2_done"), ]) } } diff --git a/Tests/RxFeedbackTests/ReactNonEquatableLoopsTests.swift b/Tests/RxFeedbackTests/ReactNonEquatableLoopsTests.swift deleted file mode 100644 index eae6c6b..0000000 --- a/Tests/RxFeedbackTests/ReactNonEquatableLoopsTests.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// ReactNonEquatableLoopsTests.swift -// RxFeedback -// -// Created by Alexander Sokol on 23/09/2017. -// Copyright © 2017 Krunoslav Zaher. All rights reserved. -// - -import Foundation -import XCTest -import RxFeedback -import RxSwift -import RxTest - -class ReactNonNilLoopsTests: RxTest { -} - -// Tests on the react function with not an equatable or hashable Control. -extension ReactNonNilLoopsTests { - - func testIntialNilQueryDoesNotProduceEffects() { - // Prepare - let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> ()? = { _ in - return nil - } - let effects: () -> Observable = { .just("_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) - let system = Observable.system( - initialState: "initial", - reduce: { oldState, append in - return oldState + append - }, - scheduler: scheduler, - scheduledFeedback: feedback - ) - - // Run - let results = scheduler.start { system } - - // Test - XCTAssertEqual(results.events, [ - next(201, "initial") - ]) - } - - func testNotNilAfterIntialNilDoesProduceEffects() { - // Prepare - let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> ()? = { state in - if state == "initial+" { - return () - } else { - return nil - } - } - let effects: () -> Observable = { .just("_a") } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) - let mutations = PublishSubject() - let system = Observable.system( - initialState: "initial", - reduce: { oldState, append in - return oldState + append - }, - scheduler: scheduler, - scheduledFeedback: feedback, { _ in mutations.asObservable() } - ) - - // Run - scheduler.scheduleAt(210) { mutations.onNext("+") } - let results = scheduler.start { system } - - // Test - XCTAssertEqual(results.events, [ - next(201, "initial"), - next(211, "initial+"), - next(213, "initial+_a"), - ]) - } - - func testSecondConsecutiveNotNilQueryDoesNotProduceEffects() { - // Prepare - let scheduler = TestScheduler(initialClock: 0) - let query: (String) -> ()? = { _ in return () } - let effects: () -> Observable = { - return .just("_a") - } - let feedback: (ObservableSchedulerContext) -> Observable = react(query: query, effects: effects) - let system = Observable.system( - initialState: "initial", - reduce: { oldState, append in - return oldState + append - }, - scheduler: scheduler, - scheduledFeedback: feedback - ) - - // Run - let results = scheduler.start { system } - - // Test - XCTAssertEqual(results.events, [ - next(201, "initial"), - next(204, "initial_a") - ]) - } - - func testImmediateEffectsHaveTheSameOrderAsTheyArePassedToSystem() { - // Prepare - let scheduler = TestScheduler(initialClock: 0) - let query1: (String) -> ()? = { state in - if state == "initial" { - return () - } else { - return nil - } - } - let query2: (String) -> ()? = { state in - if state == "initial_a" { - return () - } else { - return nil - } - } - let effects1: () -> Observable = { - return .just("_a") - } - let effects2: () -> Observable = { - return .just("_b") - } - let feedback1: (ObservableSchedulerContext) -> Observable - feedback1 = react(query: query1, effects: effects1) - let feedback2: (ObservableSchedulerContext) -> Observable - feedback2 = react(query: query2, effects: effects2) - let system = Observable.system( - initialState: "initial", - reduce: { oldState, append in - return oldState + append - }, - scheduler: scheduler, - scheduledFeedback: feedback1, feedback2 - ) - - // Run - let results = scheduler.start { system } - - // Test - XCTAssertEqual(results.events, [ - next(201, "initial"), - next(204, "initial_a"), - next(206, "initial_a_b") - ]) - } - - func testFeedbacksCancelation() { - // Prepare - let scheduler = TestScheduler(initialClock: 0) - let notImmediateEffect = PublishSubject() - let query1: (String) -> ()? = { state in - if state == "initial" { - return () - } else { - return nil - } - } - let query2: (String) -> ()? = { state in - if state == "initial" { - return () - } else { - return nil - } - } - var isEffects1Called = false - let effects1: () -> Observable = { - isEffects1Called = true - return notImmediateEffect.asObservable() - } - let effects2: () -> Observable = { - return .just("_b") - } - let feedback1: (ObservableSchedulerContext) -> Observable - feedback1 = react(query: query1, effects: effects1) - let feedback2: (ObservableSchedulerContext) -> Observable - feedback2 = react(query: query2, effects: effects2) - let system = Observable.system( - initialState: "initial", - reduce: { oldState, append in - return oldState + append - }, - scheduler: scheduler, - scheduledFeedback: feedback1, feedback2 - ) - - // Run - scheduler.scheduleAt(210) { notImmediateEffect.onNext("_a") } - let results = scheduler.start { system } - - // Test - XCTAssertEqual(results.events, [ - next(201, "initial"), - next(204, "initial_b") - ]) - XCTAssertTrue(isEffects1Called) - } -} diff --git a/Tests/RxFeedbackTests/RxFeedbackDriverTests.swift b/Tests/RxFeedbackTests/RxFeedbackDriverTests.swift index ce8d1e1..e04f0d3 100644 --- a/Tests/RxFeedbackTests/RxFeedbackDriverTests.swift +++ b/Tests/RxFeedbackTests/RxFeedbackDriverTests.swift @@ -123,7 +123,7 @@ extension RxFeedbackDriverTests { // Feedback loops extension RxFeedbackDriverTests { func testImmediateFeedbackLoopParallel_react_non_equatable() { - let feedbackLoop: (Driver) -> Signal = react(query: { $0.needsToAppendDot }) { (_: ()) -> Signal in + let feedbackLoop: (Driver) -> Signal = react(request: { $0.needsToAppendDot }) { (_: Bool) -> Signal in return Signal.just("_.") } @@ -152,7 +152,7 @@ extension RxFeedbackDriverTests { } func testImmediateFeedbackLoopParallel_react_equatable() { - let feedbackLoop: (Driver) -> Signal = react(query: { $0.needsToAppend }) { value in + let feedbackLoop: (Driver) -> Signal = react(request: { $0.needsToAppend }) { value in return Signal.just(value) } @@ -181,7 +181,7 @@ extension RxFeedbackDriverTests { } func testImmediateFeedbackLoopParallel_react_set() { - let feedbackLoop: (Driver) -> Signal = react(query: { $0.needsToAppendParallel }) { value in + let feedbackLoop: (Driver) -> Signal = react(requests: { $0.needsToAppendParallel }) { value in return Signal.just(value) } @@ -329,7 +329,7 @@ extension RxFeedbackDriverTests { SharingScheduler.mock(scheduler: testScheduler) { let player: Feedback = react( - query: { $0 }, effects: { _ in + request: { $0 }, effects: { _ in timer.map { _ in 1 } }) @@ -357,9 +357,9 @@ extension RxFeedbackDriverTests { let correct = [ next(2, 0), - next(57, 1), - next(111, 2), - next(165, 3), + next(56, 1), + next(109, 2), + next(162, 3), ] XCTAssertEqual(testableObserver.events, correct) XCTAssertEqual(subscriptionState, [0, 1, 2, 3]) @@ -385,7 +385,7 @@ extension RxFeedbackDriverTests { .asDriver(onErrorJustReturn: []) .do(onDispose: { happened(.disposedSource) }) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Driver) -> Signal in happened(.effects(calledWithInitial: initial)) return state @@ -435,7 +435,7 @@ extension RxFeedbackDriverTests { .asDriver(onErrorJustReturn: []) .do(onDispose: { happened(.disposedSource) }) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Driver) -> Signal in happened(.effects(calledWithInitial: initial)) return state.map { "Got \($0.value)" } @@ -479,7 +479,7 @@ extension RxFeedbackDriverTests { .asDriver(onErrorJustReturn: []) .do(onDispose: { happened(.disposedSource) }) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Driver) -> Signal in happened(.effects(calledWithInitial: initial)) return state.asObservable() @@ -497,7 +497,7 @@ extension RxFeedbackDriverTests { )(source).asObservable() } - XCTAssertEqual(verify, [ + XCTAssertTrue(verify == [ Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), Recorded(time: 211, value: SignificantEvent.subscribed(id: 0)), Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), @@ -505,11 +505,22 @@ extension RxFeedbackDriverTests { Recorded(time: 222, value: SignificantEvent.disposed(id: 0)), Recorded(time: 1000, value: SignificantEvent.disposed(id: 1)), Recorded(time: 1000, value: SignificantEvent.disposedSource), + ] || verify == [ + Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), + Recorded(time: 211, value: SignificantEvent.subscribed(id: 1)), + Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), + Recorded(time: 211, value: SignificantEvent.subscribed(id: 0)), + Recorded(time: 222, value: SignificantEvent.disposed(id: 0)), + Recorded(time: 1000, value: SignificantEvent.disposed(id: 1)), + Recorded(time: 1000, value: SignificantEvent.disposedSource), ]) - XCTAssertEqual(results.events, [ + XCTAssertTrue(results.events == [ next(215, "Got 1"), next(216, "Got 2"), + ] || results.events == [ + next(215, "Got 2"), + next(216, "Got 1"), ]) } } @@ -530,7 +541,7 @@ extension RxFeedbackDriverTests { .asDriver(onErrorJustReturn: []) .do(onDispose: { happened(.disposedSource) }) let result = react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Driver) -> Signal in happened(.effects(calledWithInitial: initial)) return state @@ -556,7 +567,7 @@ extension RxFeedbackDriverTests { } } - XCTAssertEqual(verify, [ + XCTAssertTrue(verify == [ Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), Recorded(time: 211, value: SignificantEvent.subscribed(id: 0)), Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), @@ -564,11 +575,22 @@ extension RxFeedbackDriverTests { Recorded(time: 220, value: SignificantEvent.disposed(id: -1)), Recorded(time: 220, value: SignificantEvent.disposed(id: -1)), Recorded(time: 220, value: SignificantEvent.disposedSource), + ] || verify == [ + Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), + Recorded(time: 211, value: SignificantEvent.subscribed(id: 1)), + Recorded(time: 211, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), + Recorded(time: 211, value: SignificantEvent.subscribed(id: 0)), + Recorded(time: 220, value: SignificantEvent.disposed(id: -1)), + Recorded(time: 220, value: SignificantEvent.disposed(id: -1)), + Recorded(time: 220, value: SignificantEvent.disposedSource), ]) - XCTAssertEqual(results.events, [ + XCTAssertTrue(results.events == [ next(215, "Got 1"), next(216, "Got 2"), + ] || results.events == [ + next(215, "Got 2"), + next(216, "Got 1"), ]) } } diff --git a/Tests/RxFeedbackTests/RxFeedbackObservableTests.swift b/Tests/RxFeedbackTests/RxFeedbackObservableTests.swift index 237a622..8b4d98f 100644 --- a/Tests/RxFeedbackTests/RxFeedbackObservableTests.swift +++ b/Tests/RxFeedbackTests/RxFeedbackObservableTests.swift @@ -149,7 +149,7 @@ extension RxFeedbackObservableTests { // Feedback loops extension RxFeedbackObservableTests { func testImmediateFeedbackLoopParallel_react_non_equatable() { - let feedbackLoop: (ObservableSchedulerContext) -> Observable = react(query: { $0.needsToAppendDot }) { _ in + let feedbackLoop: (ObservableSchedulerContext) -> Observable = react(request: { $0.needsToAppendDot }) { _ in return Observable.just("_.") } @@ -179,7 +179,7 @@ extension RxFeedbackObservableTests { } func testImmediateFeedbackLoopParallel_react_equatable() { - let feedbackLoop: (ObservableSchedulerContext) -> Observable = react(query: { $0.needsToAppend }) { value in + let feedbackLoop: (ObservableSchedulerContext) -> Observable = react(request: { $0.needsToAppend }) { value in return Observable.just(value) } @@ -209,7 +209,7 @@ extension RxFeedbackObservableTests { } func testImmediateFeedbackLoopParallel_react_set() { - let feedbackLoop: (ObservableSchedulerContext) -> Observable = react(query: { $0.needsToAppendParallel }) { value in + let feedbackLoop: (ObservableSchedulerContext) -> Observable = react(requests: { $0.needsToAppendParallel }) { value in return Observable.just(value) } @@ -356,10 +356,7 @@ extension RxFeedbackObservableTests { var subscriptionState: [Int] = [] var subscriptionIsDisposed = false - let player: Feedback = react( - query: { $0 }, effects: { _ in - timer.map { _ in 1 } - }) + let player: Feedback = react(request: { $0 }, effects: { _ in timer.map { _ in 1 } }) let mockUIBindings: Feedback = RxFeedback.bind { state in let subscriptions: [Disposable] = [ @@ -388,9 +385,9 @@ extension RxFeedbackObservableTests { testScheduler.start() let correct = [ next(1, 0), - next(54, 1), - next(106, 2), - next(158, 3), + next(53, 1), + next(104, 2), + next(155, 3), ] XCTAssertEqual(observer.events, correct) XCTAssertEqual(subscriptionState, [0, 1, 2, 3]) @@ -431,7 +428,7 @@ extension RxFeedbackObservableTests { scheduler: testScheduler ) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Observable) -> Observable in happened(.effects(calledWithInitial: initial)) return state.map { "Got \($0.value)" } @@ -481,7 +478,7 @@ extension RxFeedbackObservableTests { scheduler: testScheduler ) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier }}, effects: { (initial: TestChild, state: Observable) -> Observable in happened(.effects(calledWithInitial: initial)) return state.map { "Got \($0.value)" } @@ -526,7 +523,7 @@ extension RxFeedbackObservableTests { scheduler: testScheduler ) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Observable) -> Observable in happened(.effects(calledWithInitial: initial)) return state @@ -571,7 +568,7 @@ extension RxFeedbackObservableTests { scheduler: testScheduler ) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Observable) -> Observable in happened(.effects(calledWithInitial: initial)) return state.map { @@ -586,7 +583,7 @@ extension RxFeedbackObservableTests { )(context) } - XCTAssertEqual(verify, [ + XCTAssertTrue(verify == [ Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), Recorded(time: 210, value: SignificantEvent.subscribed(id: 0)), Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), @@ -594,9 +591,21 @@ extension RxFeedbackObservableTests { Recorded(time: 220, value: SignificantEvent.disposed(id: 0)), Recorded(time: 221, value: SignificantEvent.disposedSource), Recorded(time: 221, value: SignificantEvent.disposed(id: 1)) + ] || verify == [ + Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), + Recorded(time: 210, value: SignificantEvent.subscribed(id: 1)), + Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), + Recorded(time: 210, value: SignificantEvent.subscribed(id: 0)), + Recorded(time: 220, value: SignificantEvent.disposed(id: 0)), + Recorded(time: 221, value: SignificantEvent.disposedSource), + Recorded(time: 221, value: SignificantEvent.disposed(id: 1)) ]) - XCTAssertEqual(results.events, [ + XCTAssertTrue(results.events == [ + next(211, "Got 1"), + next(211, "Got 2"), + error(221, TestError.error1) + ] || results.events == [ next(211, "Got 1"), next(211, "Got 2"), error(221, TestError.error1) @@ -623,7 +632,7 @@ extension RxFeedbackObservableTests { scheduler: testScheduler ) return react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Observable) -> Observable in happened(.effects(calledWithInitial: initial)) return state.map { childState -> Event in @@ -639,7 +648,7 @@ extension RxFeedbackObservableTests { )(context) } - XCTAssertEqual(verify, [ + XCTAssertTrue(verify == [ Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), Recorded(time: 210, value: SignificantEvent.subscribed(id: 0)), Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), @@ -647,11 +656,22 @@ extension RxFeedbackObservableTests { Recorded(time: 220, value: SignificantEvent.disposed(id: 0)), Recorded(time: 1000, value: SignificantEvent.disposed(id: 1)), Recorded(time: 1000, value: SignificantEvent.disposedSource), + ] || verify == [ + Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), + Recorded(time: 210, value: SignificantEvent.subscribed(id: 1)), + Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), + Recorded(time: 210, value: SignificantEvent.subscribed(id: 0)), + Recorded(time: 220, value: SignificantEvent.disposed(id: 0)), + Recorded(time: 1000, value: SignificantEvent.disposed(id: 1)), + Recorded(time: 1000, value: SignificantEvent.disposedSource), ]) - XCTAssertEqual(results.events, [ + XCTAssertTrue(results.events == [ next(211, "Got 1"), next(211, "Got 2"), + ] || results.events == [ + next(211, "Got 2"), + next(211, "Got 1"), ]) } @@ -674,7 +694,7 @@ extension RxFeedbackObservableTests { scheduler: testScheduler ) let result = react( - childQuery: { (state: [TestChild]) in state }, + requests: { (state: [TestChild]) in state.indexBy { $0.identifier } }, effects: { (initial: TestChild, state: Observable) -> Observable in happened(.effects(calledWithInitial: initial)) return state.map { childState -> Event in @@ -700,7 +720,15 @@ extension RxFeedbackObservableTests { } } - XCTAssertEqual(verify, [ + XCTAssertTrue(verify == [ + Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), + Recorded(time: 210, value: SignificantEvent.subscribed(id: 0)), + Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), + Recorded(time: 210, value: SignificantEvent.subscribed(id: 1)), + Recorded(time: 220, value: SignificantEvent.disposed(id: -1)), + Recorded(time: 220, value: SignificantEvent.disposed(id: -1)), + Recorded(time: 220, value: SignificantEvent.disposedSource), + ] || verify == [ Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 0, value: "1"))), Recorded(time: 210, value: SignificantEvent.subscribed(id: 0)), Recorded(time: 210, value: SignificantEvent.effects(calledWithInitial: TestChild(identifier: 1, value: "2"))), @@ -710,14 +738,23 @@ extension RxFeedbackObservableTests { Recorded(time: 220, value: SignificantEvent.disposedSource), ]) - XCTAssertEqual(results.events, [ + XCTAssertTrue(results.events == [ next(211, "Got 1"), next(211, "Got 2"), - ]) + ] || results.events == [ + next(211, "Got 2"), + next(211, "Got 1"), + ]) + } +} + +extension Array { + func indexBy(_ keySelector: (Element) -> Key) -> [Key: Element] { + return Dictionary(self.map { (keySelector($0), $0) }, uniquingKeysWith: { first, _ in first }) } } -struct TestChild: Equatable, Identifiable { +struct TestChild: Equatable { var identifier: Int var value: String } diff --git a/Tests/RxFeedbackTests/String+Test.swift b/Tests/RxFeedbackTests/String+Test.swift index 315b783..c3f8290 100644 --- a/Tests/RxFeedbackTests/String+Test.swift +++ b/Tests/RxFeedbackTests/String+Test.swift @@ -9,9 +9,9 @@ import Foundation extension String { - var needsToAppendDot: ()? { + var needsToAppendDot: Bool? { if self == "initial" || self == "initial_." || self == "initial_._." { - return () + return true } else { return nil