Skip to content

Commit 1892541

Browse files
committed
Deprecate the EffectHandler protocol
1 parent f1d5683 commit 1892541

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
@@ -4,19 +4,29 @@
44
import Foundation
55

66
final class EffectExecutor<Effect, Event>: Connectable {
7-
private let handleEffect: (Effect, EffectCallback<Event>) -> Disposable
7+
enum Operation {
8+
case eventEmitting((Effect, EffectCallback<Event>) -> Disposable)
9+
case eventReturning((Effect) -> Event?)
10+
case sideEffecting((Effect) -> Void)
11+
}
12+
13+
private let operation: Operation
814
private var output: Consumer<Event>?
915

1016
private let lock = Lock()
1117

1218
// Keep track of each received effect's state.
1319
// When an effect has completed, it should be removed from this dictionary.
1420
// When disposing this effect handler, all entries must be removed.
15-
private var handlingEffects: [Int64: EffectHandlingState<Event>] = [:]
21+
private var ongoingEffects: [Int64: EffectHandlingState<Event>] = [:]
1622
private var nextID = Int64(0)
1723

18-
init(handleInput: @escaping (Effect, EffectCallback<Event>) -> Disposable) {
19-
self.handleEffect = handleInput
24+
init(operation: Operation) {
25+
self.operation = operation
26+
}
27+
28+
deinit {
29+
dispose()
2030
}
2131

2232
func connect(_ consumer: @escaping Consumer<Event>) -> Connection<Effect> {
@@ -38,7 +48,31 @@ final class EffectExecutor<Effect, Event>: Connectable {
3848
}
3949
}
4050

41-
func handle(_ effect: Effect) {
51+
private func handle(_ effect: Effect) {
52+
switch operation {
53+
case .eventEmitting(let handler): handleOngoing(effect, handler: handler)
54+
case .eventReturning(let handler): handler(effect).map { event in output?(event) }
55+
case .sideEffecting(let handler): handler(effect)
56+
}
57+
}
58+
59+
private func dispose() {
60+
lock.synchronized {
61+
// Dispose any effects currently being handled. We also need to `end` their callbacks to remove the
62+
// references we are keeping to them.
63+
ongoingEffects.values
64+
.forEach {
65+
$0.disposable.dispose()
66+
$0.callback.end()
67+
}
68+
69+
// Restore the state of this `Connectable` to its pre-connected state.
70+
ongoingEffects = [:]
71+
output = nil
72+
}
73+
}
74+
75+
private func handleOngoing(_ effect: Effect, handler: @escaping (Effect, EffectCallback<Event>) -> Disposable) {
4276
let id: Int64 = lock.synchronized {
4377
nextID += 1
4478
return nextID
@@ -52,47 +86,27 @@ final class EffectExecutor<Effect, Event>: Connectable {
5286
onEnd: { [weak self] in self?.delete(id: id) }
5387
)
5488

55-
let disposable = handleEffect(effect, callback)
56-
89+
let disposable = handler(effect, callback)
5790
store(id: id, callback: callback, disposable: disposable)
91+
5892
// We cannot know if `callback.end()` was called before `self.store(..)`. This check ensures that if
5993
// the callback was ended early, the reference to it will be deleted.
6094
if callback.ended {
6195
delete(id: id)
6296
}
6397
}
6498

65-
func dispose() {
66-
lock.synchronized {
67-
// Dispose any effects currently being handled. We also need to `end` their callbacks to remove the
68-
// references we are keeping to them.
69-
handlingEffects.values
70-
.forEach {
71-
$0.disposable.dispose()
72-
$0.callback.end()
73-
}
74-
75-
// Restore the state of this `Connectable` to its pre-connected state.
76-
handlingEffects = [:]
77-
output = nil
78-
}
79-
}
80-
8199
private func store(id: Int64, callback: EffectCallback<Event>, disposable: Disposable) {
82100
lock.synchronized {
83-
handlingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable)
101+
ongoingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable)
84102
}
85103
}
86104

87105
private func delete(id: Int64) {
88106
lock.synchronized {
89-
handlingEffects[id] = nil
107+
ongoingEffects[id] = nil
90108
}
91109
}
92-
93-
deinit {
94-
dispose()
95-
}
96110
}
97111

98112
private struct EffectHandlingState<Event> {

MobiusCore/Source/EffectHandlers/EffectHandler.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/// call `callback.end()`.
1010
///
1111
/// Note: `EffectHandler` should be used in conjunction with an `EffectRouter`.
12+
@available(*, deprecated)
1213
public protocol EffectHandler {
1314
associatedtype EffectParameters
1415
associatedtype Event
@@ -33,6 +34,7 @@ public protocol EffectHandler {
3334
}
3435

3536
/// A type-erased wrapper of the `EffectHandler` protocol.
37+
@available(*, deprecated)
3638
public struct AnyEffectHandler<EffectParameters, Event>: EffectHandler {
3739
private let handleClosure: (EffectParameters, EffectCallback<Event>) -> Disposable
3840

MobiusCore/Source/EffectHandlers/EffectRouter.swift

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

70+
func routed<C: Connectable>(
71+
_ connectable: C
72+
) -> EffectRouter<Effect, Event> where C.Input == EffectParameters, C.Output == Event {
73+
let route = Route(extractParameters: path, connectable: connectable, queue: queue)
74+
return EffectRouter(routes: routes + [route])
75+
}
76+
7077
/// Route to an `EffectHandler`.
7178
///
7279
/// - Parameter effectHandler: the `EffectHandler` for the route in question.
80+
@available(*, deprecated, message: "prefer routing directly to the handling closure, eg: .to(myEffectHandler.handle)")
7381
public func to<Handler: EffectHandler>(
7482
_ effectHandler: Handler
7583
) -> EffectRouter<Effect, Event> where Handler.EffectParameters == EffectParameters, Handler.Event == Event {
76-
let connectable = EffectExecutor(handleInput: effectHandler.handle)
77-
let route = Route<Effect, Event>(extractParameters: path, connectable: connectable, queue: queue)
78-
return EffectRouter(routes: routes + [route])
84+
return routed(EffectExecutor(operation: .eventEmitting(effectHandler.handle)))
7985
}
8086

8187
/// Route to a Connectable.
@@ -84,9 +90,7 @@ public struct _PartialEffectRouter<Effect, EffectParameters, Event> {
8490
public func to<C: Connectable>(
8591
_ connectable: C
8692
) -> EffectRouter<Effect, Event> where C.Input == EffectParameters, C.Output == Event {
87-
let connectable = ThreadSafeConnectable(connectable: connectable)
88-
let route = Route(extractParameters: path, connectable: connectable, queue: queue)
89-
return EffectRouter(routes: routes + [route])
93+
return routed(ThreadSafeConnectable(connectable: connectable))
9094
}
9195

9296
/// 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
@@ -19,7 +19,7 @@ public extension _PartialEffectRouter {
1919
func to(
2020
_ handle: @escaping (EffectParameters, EffectCallback<Event>) -> Disposable
2121
) -> EffectRouter<Effect, Event> {
22-
return to(AnyEffectHandler(handle: handle))
22+
return routed(EffectExecutor(operation: .eventEmitting(handle)))
2323
}
2424

2525
/// Route to a side-effecting closure.
@@ -28,11 +28,7 @@ public extension _PartialEffectRouter {
2828
func to(
2929
_ fireAndForget: @escaping (EffectParameters) -> Void
3030
) -> EffectRouter<Effect, Event> {
31-
return to { parameters, callback in
32-
fireAndForget(parameters)
33-
callback.end()
34-
return AnonymousDisposable {}
35-
}
31+
return routed(EffectExecutor(operation: .sideEffecting(fireAndForget)))
3632
}
3733

3834
/// Route to a closure which returns an optional event when given the parameters as input.
@@ -42,12 +38,6 @@ public extension _PartialEffectRouter {
4238
func toEvent(
4339
_ eventClosure: @escaping (EffectParameters) -> Event?
4440
) -> EffectRouter<Effect, Event> {
45-
return to { parameters, callback in
46-
if let event = eventClosure(parameters) {
47-
callback.send(event)
48-
}
49-
callback.end()
50-
return AnonymousDisposable {}
51-
}
41+
return routed(EffectExecutor(operation: .eventReturning(eventClosure)))
5242
}
5343
}

MobiusCore/Test/EffectHandlers/AnyEffectHandlerTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Quick
88
private typealias Effect = String
99
private typealias Event = String
1010

11+
@available(*, deprecated)
1112
class AnyEffectHandlerTests: QuickSpec {
1213
// swiftlint:disable:next function_body_length
1314
override func spec() {
@@ -48,7 +49,7 @@ class AnyEffectHandlerTests: QuickSpec {
4849

4950
context("when initialized with wrapped effect handler") {
5051
beforeEach {
51-
let wrapped = TestEffectHandler()
52+
let wrapped = WrappedEffectHandler()
5253
effectHandler = AnyEffectHandler(handler: wrapped)
5354
}
5455

@@ -57,7 +58,7 @@ class AnyEffectHandlerTests: QuickSpec {
5758

5859
context("when initialized with doubly wrapped effect handler") {
5960
beforeEach {
60-
let wrapped = TestEffectHandler()
61+
let wrapped = WrappedEffectHandler()
6162
let inner = AnyEffectHandler(handler: wrapped)
6263
effectHandler = AnyEffectHandler(handler: inner)
6364
}
@@ -68,7 +69,8 @@ class AnyEffectHandlerTests: QuickSpec {
6869
}
6970
}
7071

71-
private struct TestEffectHandler: EffectHandler {
72+
@available(*, deprecated)
73+
private struct WrappedEffectHandler: EffectHandler {
7274
func handle(_ effect: Effect, _ callback: EffectCallback<Event>) -> Disposable {
7375
callback.send(effect)
7476
return AnonymousDisposable {

MobiusCore/Test/EffectHandlers/EffectHandlerTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ private enum Event {
1919
class EffectHandlerTests: QuickSpec {
2020
override func spec() {
2121
describe("Handling effects with EffectHandler") {
22-
var effectHandler: AnyEffectHandler<Effect, Event>!
22+
var effectHandler: TestEffectHandler<Effect, Event>!
2323
var executeEffect: ((Effect) -> Void)!
2424
var receivedEvents: [Event]!
2525

2626
beforeEach {
27-
effectHandler = AnyEffectHandler(handle: handleEffect)
27+
effectHandler = handleEffect
2828
receivedEvents = []
2929
let callback = EffectCallback(
3030
onSend: { event in
@@ -33,7 +33,7 @@ class EffectHandlerTests: QuickSpec {
3333
onEnd: {}
3434
)
3535
executeEffect = { effect in
36-
_ = effectHandler.handle(effect, callback)
36+
_ = effectHandler(effect, callback)
3737
}
3838
}
3939

@@ -53,13 +53,13 @@ class EffectHandlerTests: QuickSpec {
5353
describe("Disposing EffectHandler") {
5454
it("calls the returned disposable when disposing") {
5555
var disposed = false
56-
let effectHandler = AnyEffectHandler<Effect, Event> { _, _ in
56+
let effectHandler: TestEffectHandler<Effect, Event> = { _, _ in
5757
AnonymousDisposable {
5858
disposed = true
5959
}
6060
}
6161
let callback = EffectCallback<Event>(onSend: { _ in }, onEnd: {})
62-
effectHandler.handle(.effect1, callback).dispose()
62+
effectHandler(.effect1, callback).dispose()
6363

6464
expect(disposed).to(beTrue())
6565
}

MobiusCore/Test/EffectHandlers/EffectRouterTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ class EffectRouterTests: QuickSpec {
3232
receivedEvents = []
3333
disposed1 = false
3434
disposed2 = false
35-
let effectHandler1 = AnyEffectHandler<Effect, Event> { _, callback in
35+
let effectHandler1: TestEffectHandler<Effect, Event> = { _, callback in
3636
callback.send(.eventForEffect1)
3737
return AnonymousDisposable {
3838
disposed1 = true
3939
}
4040
}
41-
let effectHandler2 = AnyEffectHandler<Effect, Event> { _, callback in
41+
let effectHandler2: TestEffectHandler<Effect, Event> = { _, callback in
4242
callback.send(.eventForEffect2)
4343
return AnonymousDisposable {
4444
disposed2 = true
@@ -107,7 +107,7 @@ class EffectRouterTests: QuickSpec {
107107
var dispose: (() -> Void)!
108108

109109
beforeEach {
110-
let handler = AnyEffectHandler<Effect, Event> { _, _ in
110+
let handler: TestEffectHandler<Effect, Event> = { _, _ in
111111
AnonymousDisposable {}
112112
}
113113
let invalidRouter = EffectRouter<Effect, Event>()

MobiusCore/Test/EventRouterDisposalLogicalRaceRegressionTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ private class EffectCollaborator {
8888
}
8989

9090
extension EffectCollaborator {
91-
func makeEffectHandler<Effect, Event>(replyEvent: Event) -> AnyEffectHandler<Effect, Event> {
92-
return AnyEffectHandler<Effect, Event> { _, callback in
91+
func makeEffectHandler<Effect, Event>(replyEvent: Event) -> TestEffectHandler<Effect, Event> {
92+
return { _, callback in
9393
let cancellationToken = self.asyncDoStuff {
9494
callback.send(replyEvent)
9595
callback.end()

MobiusCore/Test/MobiusLoopTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ class MobiusLoopTests: QuickSpec {
216216
beforeEach {
217217
disposed.value = false
218218
didReceiveEffect.value = false
219-
let effectHandler = AnyEffectHandler<Int, Int> { _, _ in
219+
let effectHandler: TestEffectHandler<Int, Int> = { _, _ in
220220
didReceiveEffect.value = true
221221
return AnonymousDisposable {
222222
disposed.value = true

MobiusCore/Test/NonReentrancyTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class NonReentrancyTests: QuickSpec {
5454
}
5555
}
5656

57-
let testEffectHandler = AnyEffectHandler<Effect, Event> {
57+
let testEffectHandler: TestEffectHandler<Effect, Event> = {
5858
handleEffect($0, $1)
5959
return AnonymousDisposable {}
6060
}

MobiusCore/Test/TestingUtil.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Foundation
55
@testable import MobiusCore
66
import Nimble
77

8+
typealias TestEffectHandler<EffectParameters, Event> = (EffectParameters, EffectCallback<Event>) -> Disposable
9+
810
class SimpleTestConnectable: Connectable {
911
var disposed = false
1012

0 commit comments

Comments
 (0)