Skip to content

Commit 6dfa6ac

Browse files
committed
Deprecate the EffectHandler protocol
1 parent 315a69a commit 6dfa6ac

11 files changed

+77
-63
lines changed

MobiusCore/Source/EffectHandlers/EffectExecutor.swift

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,29 @@
1515
import Foundation
1616

1717
final class EffectExecutor<Effect, Event>: Connectable {
18-
private let handleEffect: (Effect, EffectCallback<Event>) -> Disposable
18+
enum Operation {
19+
case asynchronous((Effect, EffectCallback<Event>) -> Disposable)
20+
case event((Effect) -> Event?)
21+
case fire((Effect) -> Void)
22+
}
23+
24+
private let operation: Operation
1925
private var output: Consumer<Event>?
2026

2127
private let lock = Lock()
2228

2329
// Keep track of each received effect's state.
2430
// When an effect has completed, it should be removed from this dictionary.
2531
// When disposing this effect handler, all entries must be removed.
26-
private var handlingEffects: [Int64: EffectHandlingState<Event>] = [:]
32+
private var ongoingEffects: [Int64: EffectHandlingState<Event>] = [:]
2733
private var nextID = Int64(0)
2834

29-
init(handleInput: @escaping (Effect, EffectCallback<Event>) -> Disposable) {
30-
self.handleEffect = handleInput
35+
init(operation: Operation) {
36+
self.operation = operation
37+
}
38+
39+
deinit {
40+
dispose()
3141
}
3242

3343
func connect(_ consumer: @escaping Consumer<Event>) -> Connection<Effect> {
@@ -49,7 +59,31 @@ final class EffectExecutor<Effect, Event>: Connectable {
4959
}
5060
}
5161

52-
func handle(_ effect: Effect) {
62+
private func handle(_ effect: Effect) {
63+
switch operation {
64+
case .asynchronous(let handler): handleOngoing(effect, handler: handler)
65+
case .event(let handler): handler(effect).map { event in output?(event) }
66+
case .fire(let handler): handler(effect)
67+
}
68+
}
69+
70+
private func dispose() {
71+
lock.synchronized {
72+
// Dispose any effects currently being handled. We also need to `end` their callbacks to remove the
73+
// references we are keeping to them.
74+
ongoingEffects.values
75+
.forEach {
76+
$0.disposable.dispose()
77+
$0.callback.end()
78+
}
79+
80+
// Restore the state of this `Connectable` to its pre-connected state.
81+
ongoingEffects = [:]
82+
output = nil
83+
}
84+
}
85+
86+
private func handleOngoing(_ effect: Effect, handler: @escaping (Effect, EffectCallback<Event>) -> Disposable) {
5387
let id: Int64 = lock.synchronized {
5488
nextID += 1
5589
return nextID
@@ -63,47 +97,27 @@ final class EffectExecutor<Effect, Event>: Connectable {
6397
onEnd: { [weak self] in self?.delete(id: id) }
6498
)
6599

66-
let disposable = handleEffect(effect, callback)
67-
100+
let disposable = handler(effect, callback)
68101
store(id: id, callback: callback, disposable: disposable)
102+
69103
// We cannot know if `callback.end()` was called before `self.store(..)`. This check ensures that if
70104
// the callback was ended early, the reference to it will be deleted.
71105
if callback.ended {
72106
delete(id: id)
73107
}
74108
}
75109

76-
func dispose() {
77-
lock.synchronized {
78-
// Dispose any effects currently being handled. We also need to `end` their callbacks to remove the
79-
// references we are keeping to them.
80-
handlingEffects.values
81-
.forEach {
82-
$0.disposable.dispose()
83-
$0.callback.end()
84-
}
85-
86-
// Restore the state of this `Connectable` to its pre-connected state.
87-
handlingEffects = [:]
88-
output = nil
89-
}
90-
}
91-
92110
private func store(id: Int64, callback: EffectCallback<Event>, disposable: Disposable) {
93111
lock.synchronized {
94-
handlingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable)
112+
ongoingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable)
95113
}
96114
}
97115

98116
private func delete(id: Int64) {
99117
lock.synchronized {
100-
handlingEffects[id] = nil
118+
ongoingEffects[id] = nil
101119
}
102120
}
103-
104-
deinit {
105-
dispose()
106-
}
107121
}
108122

109123
private struct EffectHandlingState<Event> {

MobiusCore/Source/EffectHandlers/EffectHandler.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
/// call `callback.end()`.
2121
///
2222
/// Note: `EffectHandler` should be used in conjunction with an `EffectRouter`.
23+
@available(*, deprecated)
2324
public protocol EffectHandler {
2425
associatedtype EffectParameters
2526
associatedtype Event
@@ -44,6 +45,7 @@ public protocol EffectHandler {
4445
}
4546

4647
/// A type-erased wrapper of the `EffectHandler` protocol.
48+
@available(*, deprecated)
4749
public struct AnyEffectHandler<EffectParameters, Event>: EffectHandler {
4850
private let handleClosure: (EffectParameters, EffectCallback<Event>) -> Disposable
4951

MobiusCore/Source/EffectHandlers/EffectRouter.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,21 @@ public struct _PartialEffectRouter<Effect, EffectParameters, Event> {
7878
fileprivate let path: (Effect) -> EffectParameters?
7979
fileprivate let queue: DispatchQueue?
8080

81+
func routed<C: Connectable>(
82+
_ connectable: C
83+
) -> EffectRouter<Effect, Event> where C.Input == EffectParameters, C.Output == Event {
84+
let route = Route(extractParameters: path, connectable: connectable, queue: queue)
85+
return EffectRouter(routes: routes + [route])
86+
}
87+
8188
/// Route to an `EffectHandler`.
8289
///
8390
/// - Parameter effectHandler: the `EffectHandler` for the route in question.
91+
@available(*, deprecated, message: "prefer routing directly to the handling closure, eg: .to(myEffectHandler.handle)")
8492
public func to<Handler: EffectHandler>(
8593
_ effectHandler: Handler
8694
) -> EffectRouter<Effect, Event> where Handler.EffectParameters == EffectParameters, Handler.Event == Event {
87-
let connectable = EffectExecutor(handleInput: effectHandler.handle)
88-
let route = Route<Effect, Event>(extractParameters: path, connectable: connectable, queue: queue)
89-
return EffectRouter(routes: routes + [route])
95+
return routed(EffectExecutor(operation: .asynchronous(effectHandler.handle)))
9096
}
9197

9298
/// Route to a Connectable.
@@ -95,9 +101,7 @@ public struct _PartialEffectRouter<Effect, EffectParameters, Event> {
95101
public func to<C: Connectable>(
96102
_ connectable: C
97103
) -> EffectRouter<Effect, Event> where C.Input == EffectParameters, C.Output == Event {
98-
let connectable = ThreadSafeConnectable(connectable: connectable)
99-
let route = Route(extractParameters: path, connectable: connectable, queue: queue)
100-
return EffectRouter(routes: routes + [route])
104+
return routed(ThreadSafeConnectable(connectable: connectable))
101105
}
102106

103107
/// Handle an the current `Effect` asynchronously on the provided `DispatchQueue`

MobiusCore/Source/EffectHandlers/EffectRouterDSL.swift

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public extension _PartialEffectRouter {
3030
func to(
3131
_ handle: @escaping (EffectParameters, EffectCallback<Event>) -> Disposable
3232
) -> EffectRouter<Effect, Event> {
33-
return to(AnyEffectHandler(handle: handle))
33+
return routed(EffectExecutor(operation: .asynchronous(handle)))
3434
}
3535

3636
/// Route to a side-effecting closure.
@@ -39,11 +39,7 @@ public extension _PartialEffectRouter {
3939
func to(
4040
_ fireAndForget: @escaping (EffectParameters) -> Void
4141
) -> EffectRouter<Effect, Event> {
42-
return to { parameters, callback in
43-
fireAndForget(parameters)
44-
callback.end()
45-
return AnonymousDisposable {}
46-
}
42+
return routed(EffectExecutor(operation: .fire(fireAndForget)))
4743
}
4844

4945
/// Route to a closure which returns an optional event when given the parameters as input.
@@ -53,12 +49,6 @@ public extension _PartialEffectRouter {
5349
func toEvent(
5450
_ eventClosure: @escaping (EffectParameters) -> Event?
5551
) -> EffectRouter<Effect, Event> {
56-
return to { parameters, callback in
57-
if let event = eventClosure(parameters) {
58-
callback.send(event)
59-
}
60-
callback.end()
61-
return AnonymousDisposable {}
62-
}
52+
return routed(EffectExecutor(operation: .event(eventClosure)))
6353
}
6454
}

MobiusCore/Test/EffectHandlers/AnyEffectHandlerTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Quick
1919
private typealias Effect = String
2020
private typealias Event = String
2121

22+
@available(*, deprecated)
2223
class AnyEffectHandlerTests: QuickSpec {
2324
// swiftlint:disable:next function_body_length
2425
override func spec() {
@@ -59,7 +60,7 @@ class AnyEffectHandlerTests: QuickSpec {
5960

6061
context("when initialized with wrapped effect handler") {
6162
beforeEach {
62-
let wrapped = TestEffectHandler()
63+
let wrapped = WrappedEffectHandler()
6364
effectHandler = AnyEffectHandler(handler: wrapped)
6465
}
6566

@@ -68,7 +69,7 @@ class AnyEffectHandlerTests: QuickSpec {
6869

6970
context("when initialized with doubly wrapped effect handler") {
7071
beforeEach {
71-
let wrapped = TestEffectHandler()
72+
let wrapped = WrappedEffectHandler()
7273
let inner = AnyEffectHandler(handler: wrapped)
7374
effectHandler = AnyEffectHandler(handler: inner)
7475
}
@@ -79,7 +80,8 @@ class AnyEffectHandlerTests: QuickSpec {
7980
}
8081
}
8182

82-
private struct TestEffectHandler: EffectHandler {
83+
@available(*, deprecated)
84+
private struct WrappedEffectHandler: EffectHandler {
8385
func handle(_ effect: Effect, _ callback: EffectCallback<Event>) -> Disposable {
8486
callback.send(effect)
8587
return AnonymousDisposable {

MobiusCore/Test/EffectHandlers/EffectHandlerTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ private enum Event {
3030
class EffectHandlerTests: QuickSpec {
3131
override func spec() {
3232
describe("Handling effects with EffectHandler") {
33-
var effectHandler: AnyEffectHandler<Effect, Event>!
33+
var effectHandler: TestEffectHandler<Effect, Event>!
3434
var executeEffect: ((Effect) -> Void)!
3535
var receivedEvents: [Event]!
3636

3737
beforeEach {
38-
effectHandler = AnyEffectHandler(handle: handleEffect)
38+
effectHandler = handleEffect
3939
receivedEvents = []
4040
let callback = EffectCallback(
4141
onSend: { event in
@@ -44,7 +44,7 @@ class EffectHandlerTests: QuickSpec {
4444
onEnd: {}
4545
)
4646
executeEffect = { effect in
47-
_ = effectHandler.handle(effect, callback)
47+
_ = effectHandler(effect, callback)
4848
}
4949
}
5050

@@ -64,13 +64,13 @@ class EffectHandlerTests: QuickSpec {
6464
describe("Disposing EffectHandler") {
6565
it("calls the returned disposable when disposing") {
6666
var disposed = false
67-
let effectHandler = AnyEffectHandler<Effect, Event> { _, _ in
67+
let effectHandler: TestEffectHandler<Effect, Event> = { _, _ in
6868
AnonymousDisposable {
6969
disposed = true
7070
}
7171
}
7272
let callback = EffectCallback<Event>(onSend: { _ in }, onEnd: {})
73-
effectHandler.handle(.effect1, callback).dispose()
73+
effectHandler(.effect1, callback).dispose()
7474

7575
expect(disposed).to(beTrue())
7676
}

MobiusCore/Test/EffectHandlers/EffectRouterTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ class EffectRouterTests: QuickSpec {
4343
receivedEvents = []
4444
disposed1 = false
4545
disposed2 = false
46-
let effectHandler1 = AnyEffectHandler<Effect, Event> { _, callback in
46+
let effectHandler1: TestEffectHandler<Effect, Event> = { _, callback in
4747
callback.send(.eventForEffect1)
4848
return AnonymousDisposable {
4949
disposed1 = true
5050
}
5151
}
52-
let effectHandler2 = AnyEffectHandler<Effect, Event> { _, callback in
52+
let effectHandler2: TestEffectHandler<Effect, Event> = { _, callback in
5353
callback.send(.eventForEffect2)
5454
return AnonymousDisposable {
5555
disposed2 = true
@@ -118,7 +118,7 @@ class EffectRouterTests: QuickSpec {
118118
var dispose: (() -> Void)!
119119

120120
beforeEach {
121-
let handler = AnyEffectHandler<Effect, Event> { _, _ in
121+
let handler: TestEffectHandler<Effect, Event> = { _, _ in
122122
AnonymousDisposable {}
123123
}
124124
let invalidRouter = EffectRouter<Effect, Event>()

MobiusCore/Test/EventRouterDisposalLogicalRaceRegressionTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ private class EffectCollaborator {
9999
}
100100

101101
extension EffectCollaborator {
102-
func makeEffectHandler<Effect, Event>(replyEvent: Event) -> AnyEffectHandler<Effect, Event> {
103-
return AnyEffectHandler<Effect, Event> { _, callback in
102+
func makeEffectHandler<Effect, Event>(replyEvent: Event) -> TestEffectHandler<Effect, Event> {
103+
return { _, callback in
104104
let cancellationToken = self.asyncDoStuff {
105105
callback.send(replyEvent)
106106
callback.end()

MobiusCore/Test/MobiusLoopTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ class MobiusLoopTests: QuickSpec {
227227
beforeEach {
228228
disposed.value = false
229229
didReceiveEffect.value = false
230-
let effectHandler = AnyEffectHandler<Int, Int> { _, _ in
230+
let effectHandler: TestEffectHandler<Int, Int> = { _, _ in
231231
didReceiveEffect.value = true
232232
return AnonymousDisposable {
233233
disposed.value = true

MobiusCore/Test/NonReentrancyTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class NonReentrancyTests: QuickSpec {
6565
}
6666
}
6767

68-
let testEffectHandler = AnyEffectHandler<Effect, Event> {
68+
let testEffectHandler: TestEffectHandler<Effect, Event> = {
6969
handleEffect($0, $1)
7070
return AnonymousDisposable {}
7171
}

MobiusCore/Test/TestingUtil.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import Foundation
1616
@testable import MobiusCore
1717
import Nimble
1818

19+
typealias TestEffectHandler<EffectParameters, Event> = (EffectParameters, EffectCallback<Event>) -> Disposable
20+
1921
class SimpleTestConnectable: Connectable {
2022
var disposed = false
2123

0 commit comments

Comments
 (0)