MobiusExtras/Source/ConnectableClass.swift (57 lines of code) (raw):
// Copyright Spotify AB.
// SPDX-License-Identifier: Apache-2.0
import Foundation
import MobiusCore
/// Superclass that allows for easy implementation of a Mobius loop `Connectable`
///
/// - Attention: Should not be used directly. Instead a subclass should be used which overrides any of combination of
/// the `handle`, `onConnect`, and `onDispose` functions.
///
/// This class automatically handles creation of `Connection`. Any subclass can override any of the following functions:
/// - `handle`: this function handles any input (i.e. T.Effects). If a `T.Event` is should be sent to the loop,
/// it should be passed to the `send` function which will pass it to the Mobius loop
///
/// - `onConnect`: this function is called when the connection is being established. It can be used to allocate and
/// initialize any resources used by the subclass.
///
/// - `onDispose`: this function is called when the loop has disposed of the `Connectable`. Any resources used by the
/// subclass should be freed here. When this function is called, the base class has already released all its
/// resources so no further functions should be run on the base class.
open class ConnectableClass<Input, Output>: Connectable {
private var consumer: Consumer<Output>?
private let lock = NSRecursiveLock()
public init() {}
/// Allows the subclass to pass data back through the established `Connection`. In the case of a Mobius loop effect
/// handler, this is the function to call to pass `T.Event` back to the loop
///
/// - Attention: This class will throw an error to the MobiusHooks error handler if a connection has not been
/// established before a call to this function is made. Setting up a connection is usually handled by the MobiusLoop
public final func send(_ output: Output) {
lock.lock()
defer {
lock.unlock()
}
guard let consumer = consumer else {
return
}
consumer(output)
}
/// Called when the `Connectable` receives input to allow the subclass to react to it.
open func handle(_ input: Input) {}
/// Called when the connection is being established. This function can be used to allocate and initialize any
/// resources used by the subclass.
open func onConnect() {}
/// Called when the connection is being disposed. This function should release any resources used by the subclass.
open func onDispose() {}
public final func connect(_ consumer: @escaping (Output) -> Void) -> Connection<Input> {
lock.lock()
defer {
lock.unlock()
}
guard self.consumer == nil else {
MobiusHooks.errorHandler(
"Connection limit exceeded: The Connectable \(type(of: self)) is already connected. " +
"Unable to connect more than once",
#file,
#line
)
}
self.consumer = consumer
onConnect()
return Connection(acceptClosure: self.accept, disposeClosure: self.dispose)
}
private func accept(_ input: Input) {
// The construct of consumerSet is there to release the lock asap.
// We don’t know what goes on in the overridden `handle` function...
var consumerSet: Bool = false
lock.lock()
consumerSet = consumer != nil
lock.unlock()
guard consumerSet else {
MobiusHooks.errorHandler(
"\(type(of: self)) is unable to handle \(type(of: input)) before any consumer has been set",
#file,
#line
)
}
handle(input)
}
private func dispose() {
lock.lock()
consumer = nil
lock.unlock()
onDispose()
}
}