diff --git a/MobiusCore/Source/EffectHandlers/EffectExecutor.swift b/MobiusCore/Source/EffectHandlers/EffectExecutor.swift index 8aed9a1b..cfa17c39 100644 --- a/MobiusCore/Source/EffectHandlers/EffectExecutor.swift +++ b/MobiusCore/Source/EffectHandlers/EffectExecutor.swift @@ -4,7 +4,13 @@ import Foundation final class EffectExecutor: Connectable { - private let handleEffect: (Effect, EffectCallback) -> Disposable + enum Operation { + case eventEmitting((Effect, EffectCallback) -> Disposable) + case eventReturning((Effect) -> Event?) + case sideEffecting((Effect) -> Void) + } + + private let operation: Operation private var output: Consumer? private let lock = Lock() @@ -12,11 +18,15 @@ final class EffectExecutor: Connectable { // Keep track of each received effect's state. // When an effect has completed, it should be removed from this dictionary. // When disposing this effect handler, all entries must be removed. - private var handlingEffects: [Int64: EffectHandlingState] = [:] + private var ongoingEffects: [Int64: EffectHandlingState] = [:] private var nextID = Int64(0) - init(handleInput: @escaping (Effect, EffectCallback) -> Disposable) { - self.handleEffect = handleInput + init(operation: Operation) { + self.operation = operation + } + + deinit { + dispose() } func connect(_ consumer: @escaping Consumer) -> Connection { @@ -38,7 +48,31 @@ final class EffectExecutor: Connectable { } } - func handle(_ effect: Effect) { + private func handle(_ effect: Effect) { + switch operation { + case .eventEmitting(let handler): handleOngoing(effect, handler: handler) + case .eventReturning(let handler): handler(effect).map { event in output?(event) } + case .sideEffecting(let handler): handler(effect) + } + } + + private func dispose() { + lock.synchronized { + // Dispose any effects currently being handled. We also need to `end` their callbacks to remove the + // references we are keeping to them. + ongoingEffects.values + .forEach { + $0.disposable.dispose() + $0.callback.end() + } + + // Restore the state of this `Connectable` to its pre-connected state. + ongoingEffects = [:] + output = nil + } + } + + private func handleOngoing(_ effect: Effect, handler: @escaping (Effect, EffectCallback) -> Disposable) { let id: Int64 = lock.synchronized { nextID += 1 return nextID @@ -52,9 +86,9 @@ final class EffectExecutor: Connectable { onEnd: { [weak self] in self?.delete(id: id) } ) - let disposable = handleEffect(effect, callback) - + let disposable = handler(effect, callback) store(id: id, callback: callback, disposable: disposable) + // We cannot know if `callback.end()` was called before `self.store(..)`. This check ensures that if // the callback was ended early, the reference to it will be deleted. if callback.ended { @@ -62,37 +96,17 @@ final class EffectExecutor: Connectable { } } - func dispose() { - lock.synchronized { - // Dispose any effects currently being handled. We also need to `end` their callbacks to remove the - // references we are keeping to them. - handlingEffects.values - .forEach { - $0.disposable.dispose() - $0.callback.end() - } - - // Restore the state of this `Connectable` to its pre-connected state. - handlingEffects = [:] - output = nil - } - } - private func store(id: Int64, callback: EffectCallback, disposable: Disposable) { lock.synchronized { - handlingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable) + ongoingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable) } } private func delete(id: Int64) { lock.synchronized { - handlingEffects[id] = nil + ongoingEffects[id] = nil } } - - deinit { - dispose() - } } private struct EffectHandlingState { diff --git a/MobiusCore/Source/EffectHandlers/EffectHandler.swift b/MobiusCore/Source/EffectHandlers/EffectHandler.swift index 0bd642ee..3aa19a6b 100644 --- a/MobiusCore/Source/EffectHandlers/EffectHandler.swift +++ b/MobiusCore/Source/EffectHandlers/EffectHandler.swift @@ -9,6 +9,7 @@ /// call `callback.end()`. /// /// Note: `EffectHandler` should be used in conjunction with an `EffectRouter`. +@available(*, deprecated) public protocol EffectHandler { associatedtype EffectParameters associatedtype Event @@ -33,6 +34,7 @@ public protocol EffectHandler { } /// A type-erased wrapper of the `EffectHandler` protocol. +@available(*, deprecated) public struct AnyEffectHandler: EffectHandler { private let handleClosure: (EffectParameters, EffectCallback) -> Disposable diff --git a/MobiusCore/Source/EffectHandlers/EffectRouter.swift b/MobiusCore/Source/EffectHandlers/EffectRouter.swift index c22aa960..193972d4 100644 --- a/MobiusCore/Source/EffectHandlers/EffectRouter.swift +++ b/MobiusCore/Source/EffectHandlers/EffectRouter.swift @@ -67,15 +67,21 @@ public struct _PartialEffectRouter { fileprivate let path: (Effect) -> EffectParameters? fileprivate let queue: DispatchQueue? + func routed( + _ connectable: C + ) -> EffectRouter where C.Input == EffectParameters, C.Output == Event { + let route = Route(extractParameters: path, connectable: connectable, queue: queue) + return EffectRouter(routes: routes + [route]) + } + /// Route to an `EffectHandler`. /// /// - Parameter effectHandler: the `EffectHandler` for the route in question. + @available(*, deprecated, message: "prefer routing directly to the handling closure, eg: .to(myEffectHandler.handle)") public func to( _ effectHandler: Handler ) -> EffectRouter where Handler.EffectParameters == EffectParameters, Handler.Event == Event { - let connectable = EffectExecutor(handleInput: effectHandler.handle) - let route = Route(extractParameters: path, connectable: connectable, queue: queue) - return EffectRouter(routes: routes + [route]) + return routed(EffectExecutor(operation: .eventEmitting(effectHandler.handle))) } /// Route to a Connectable. @@ -84,9 +90,7 @@ public struct _PartialEffectRouter { public func to( _ connectable: C ) -> EffectRouter where C.Input == EffectParameters, C.Output == Event { - let connectable = ThreadSafeConnectable(connectable: connectable) - let route = Route(extractParameters: path, connectable: connectable, queue: queue) - return EffectRouter(routes: routes + [route]) + return routed(ThreadSafeConnectable(connectable: connectable)) } /// Handle an the current `Effect` asynchronously on the provided `DispatchQueue` diff --git a/MobiusCore/Source/EffectHandlers/EffectRouterDSL.swift b/MobiusCore/Source/EffectHandlers/EffectRouterDSL.swift index 1e682d77..e903e73d 100644 --- a/MobiusCore/Source/EffectHandlers/EffectRouterDSL.swift +++ b/MobiusCore/Source/EffectHandlers/EffectRouterDSL.swift @@ -19,7 +19,7 @@ public extension _PartialEffectRouter { func to( _ handle: @escaping (EffectParameters, EffectCallback) -> Disposable ) -> EffectRouter { - return to(AnyEffectHandler(handle: handle)) + return routed(EffectExecutor(operation: .eventEmitting(handle))) } /// Route to a side-effecting closure. @@ -28,11 +28,7 @@ public extension _PartialEffectRouter { func to( _ fireAndForget: @escaping (EffectParameters) -> Void ) -> EffectRouter { - return to { parameters, callback in - fireAndForget(parameters) - callback.end() - return EmptyDisposable() - } + return routed(EffectExecutor(operation: .sideEffecting(fireAndForget))) } /// Route to a closure which returns an optional event when given the parameters as input. @@ -42,12 +38,6 @@ public extension _PartialEffectRouter { func toEvent( _ eventClosure: @escaping (EffectParameters) -> Event? ) -> EffectRouter { - return to { parameters, callback in - if let event = eventClosure(parameters) { - callback.send(event) - } - callback.end() - return EmptyDisposable() - } + return routed(EffectExecutor(operation: .eventReturning(eventClosure))) } } diff --git a/MobiusCore/Test/EffectHandlers/AnyEffectHandlerTests.swift b/MobiusCore/Test/EffectHandlers/AnyEffectHandlerTests.swift index c14f2bbf..2d1044fc 100644 --- a/MobiusCore/Test/EffectHandlers/AnyEffectHandlerTests.swift +++ b/MobiusCore/Test/EffectHandlers/AnyEffectHandlerTests.swift @@ -8,6 +8,7 @@ import Quick private typealias Effect = String private typealias Event = String +@available(*, deprecated) class AnyEffectHandlerTests: QuickSpec { // swiftlint:disable:next function_body_length override class func spec() { @@ -48,7 +49,7 @@ class AnyEffectHandlerTests: QuickSpec { context("when initialized with wrapped effect handler") { beforeEach { - let wrapped = TestEffectHandler() + let wrapped = WrappedEffectHandler() effectHandler = AnyEffectHandler(handler: wrapped) } @@ -57,7 +58,7 @@ class AnyEffectHandlerTests: QuickSpec { context("when initialized with doubly wrapped effect handler") { beforeEach { - let wrapped = TestEffectHandler() + let wrapped = WrappedEffectHandler() let inner = AnyEffectHandler(handler: wrapped) effectHandler = AnyEffectHandler(handler: inner) } @@ -68,7 +69,8 @@ class AnyEffectHandlerTests: QuickSpec { } } -private struct TestEffectHandler: EffectHandler { +@available(*, deprecated) +private struct WrappedEffectHandler: EffectHandler { func handle(_ effect: Effect, _ callback: EffectCallback) -> Disposable { callback.send(effect) return AnonymousDisposable { diff --git a/MobiusCore/Test/EffectHandlers/EffectHandlerTests.swift b/MobiusCore/Test/EffectHandlers/EffectHandlerTests.swift index 750549a6..417a64e5 100644 --- a/MobiusCore/Test/EffectHandlers/EffectHandlerTests.swift +++ b/MobiusCore/Test/EffectHandlers/EffectHandlerTests.swift @@ -19,12 +19,12 @@ private enum Event { class EffectHandlerTests: QuickSpec { override class func spec() { describe("Handling effects with EffectHandler") { - var effectHandler: AnyEffectHandler! + var effectHandler: TestEffectHandler! var executeEffect: ((Effect) -> Void)! var receivedEvents: [Event]! beforeEach { - effectHandler = AnyEffectHandler(handle: handleEffect) + effectHandler = handleEffect receivedEvents = [] let callback = EffectCallback( onSend: { event in @@ -33,7 +33,7 @@ class EffectHandlerTests: QuickSpec { onEnd: {} ) executeEffect = { effect in - _ = effectHandler.handle(effect, callback) + _ = effectHandler(effect, callback) } } @@ -53,13 +53,13 @@ class EffectHandlerTests: QuickSpec { describe("Disposing EffectHandler") { it("calls the returned disposable when disposing") { var disposed = false - let effectHandler = AnyEffectHandler { _, _ in + let effectHandler: TestEffectHandler = { _, _ in AnonymousDisposable { disposed = true } } let callback = EffectCallback(onSend: { _ in }, onEnd: {}) - effectHandler.handle(.effect1, callback).dispose() + effectHandler(.effect1, callback).dispose() expect(disposed).to(beTrue()) } diff --git a/MobiusCore/Test/EffectHandlers/EffectRouterTests.swift b/MobiusCore/Test/EffectHandlers/EffectRouterTests.swift index 366b0203..305a6a4b 100644 --- a/MobiusCore/Test/EffectHandlers/EffectRouterTests.swift +++ b/MobiusCore/Test/EffectHandlers/EffectRouterTests.swift @@ -32,13 +32,13 @@ class EffectRouterTests: QuickSpec { receivedEvents = [] disposed1 = false disposed2 = false - let effectHandler1 = AnyEffectHandler { _, callback in + let effectHandler1: TestEffectHandler = { _, callback in callback.send(.eventForEffect1) return AnonymousDisposable { disposed1 = true } } - let effectHandler2 = AnyEffectHandler { _, callback in + let effectHandler2: TestEffectHandler = { _, callback in callback.send(.eventForEffect2) return AnonymousDisposable { disposed2 = true @@ -107,7 +107,7 @@ class EffectRouterTests: QuickSpec { var dispose: (() -> Void)! beforeEach { - let handler = AnyEffectHandler { _, _ in + let handler: TestEffectHandler = { _, _ in EmptyDisposable() } let invalidRouter = EffectRouter() diff --git a/MobiusCore/Test/EventRouterDisposalLogicalRaceRegressionTest.swift b/MobiusCore/Test/EventRouterDisposalLogicalRaceRegressionTest.swift index 640593a1..af38ebe6 100644 --- a/MobiusCore/Test/EventRouterDisposalLogicalRaceRegressionTest.swift +++ b/MobiusCore/Test/EventRouterDisposalLogicalRaceRegressionTest.swift @@ -88,8 +88,8 @@ private class EffectCollaborator { } extension EffectCollaborator { - func makeEffectHandler(replyEvent: Event) -> AnyEffectHandler { - return AnyEffectHandler { _, callback in + func makeEffectHandler(replyEvent: Event) -> TestEffectHandler { + return { _, callback in let cancellationToken = self.asyncDoStuff { callback.send(replyEvent) callback.end() diff --git a/MobiusCore/Test/MobiusLoopTests.swift b/MobiusCore/Test/MobiusLoopTests.swift index fcdab10e..92f0ac60 100644 --- a/MobiusCore/Test/MobiusLoopTests.swift +++ b/MobiusCore/Test/MobiusLoopTests.swift @@ -216,7 +216,7 @@ class MobiusLoopTests: QuickSpec { beforeEach { disposed.value = false didReceiveEffect.value = false - let effectHandler = AnyEffectHandler { _, _ in + let effectHandler: TestEffectHandler = { _, _ in didReceiveEffect.value = true return AnonymousDisposable { disposed.value = true diff --git a/MobiusCore/Test/NonReentrancyTests.swift b/MobiusCore/Test/NonReentrancyTests.swift index 1ff1b0d4..d506a28e 100644 --- a/MobiusCore/Test/NonReentrancyTests.swift +++ b/MobiusCore/Test/NonReentrancyTests.swift @@ -54,7 +54,7 @@ class NonReentrancyTests: QuickSpec { } } - let testEffectHandler = AnyEffectHandler { + let testEffectHandler: TestEffectHandler = { handleEffect($0, $1) return EmptyDisposable() } diff --git a/MobiusCore/Test/TestingUtil.swift b/MobiusCore/Test/TestingUtil.swift index afc58ccc..c3eb0761 100644 --- a/MobiusCore/Test/TestingUtil.swift +++ b/MobiusCore/Test/TestingUtil.swift @@ -5,6 +5,8 @@ import Foundation @testable import MobiusCore import Nimble +typealias TestEffectHandler = (EffectParameters, EffectCallback) -> Disposable + class SimpleTestConnectable: Connectable { var disposed = false