MobiusCore/Source/EffectHandlers/EffectExecutor.swift (71 lines of code) (raw):

// Copyright Spotify AB. // SPDX-License-Identifier: Apache-2.0 import Foundation final class EffectExecutor<Effect, Event>: Connectable { private let handleEffect: (Effect, EffectCallback<Event>) -> Disposable private var output: Consumer<Event>? private let lock = Lock() // 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<Event>] = [:] private var nextID = Int64(0) init(handleInput: @escaping (Effect, EffectCallback<Event>) -> Disposable) { self.handleEffect = handleInput } func connect(_ consumer: @escaping Consumer<Event>) -> Connection<Effect> { return lock.synchronized { guard output == nil else { MobiusHooks.errorHandler( "Connection limit exceeded: The Connectable \(type(of: self)) is already connected. " + "Unable to connect more than once", #file, #line ) } output = consumer return Connection( acceptClosure: handle, disposeClosure: dispose ) } } func handle(_ effect: Effect) { let id: Int64 = lock.synchronized { nextID += 1 return nextID } let callback = EffectCallback( // Any events produced as a result of handling the effect will be sent to this class's `output` consumer, // unless it has already been disposed. onSend: { [weak self] event in self?.output?(event) }, // Once an effect has been handled, remove the reference to its callback and disposable. onEnd: { [weak self] in self?.delete(id: id) } ) let disposable = handleEffect(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 { delete(id: id) } } 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<Event>, disposable: Disposable) { lock.synchronized { handlingEffects[id] = EffectHandlingState(callback: callback, disposable: disposable) } } private func delete(id: Int64) { lock.synchronized { handlingEffects[id] = nil } } deinit { dispose() } } private struct EffectHandlingState<Event> { let callback: EffectCallback<Event> let disposable: Disposable }