376 lines
15 KiB
Swift
376 lines
15 KiB
Swift
import Dispatch
|
|
import Foundation
|
|
|
|
/// `Action` represents a repeatable work like `SignalProducer`. But on top of the
|
|
/// isolation of produced `Signal`s from a `SignalProducer`, `Action` provides
|
|
/// higher-order features like availability and mutual exclusion.
|
|
///
|
|
/// Similar to a produced `Signal` from a `SignalProducer`, each unit of the repreatable
|
|
/// work may output zero or more values, and terminate with or without an error at some
|
|
/// point.
|
|
///
|
|
/// The core of `Action` is the `execute` closure it created with. For every execution
|
|
/// attempt with a varying input, if the `Action` is enabled, it would request from the
|
|
/// `execute` closure a customized unit of work — represented by a `SignalProducer`.
|
|
/// Specifically, the `execute` closure would be supplied with the latest state of
|
|
/// `Action` and the external input from `apply()`.
|
|
///
|
|
/// `Action` enforces serial execution, and disables the `Action` during the execution.
|
|
public final class Action<Input, Output, Error: Swift.Error> {
|
|
private struct ActionState<Value> {
|
|
var isEnabled: Bool {
|
|
return isUserEnabled && !isExecuting
|
|
}
|
|
|
|
var isUserEnabled: Bool
|
|
var isExecuting: Bool
|
|
var value: Value
|
|
}
|
|
|
|
private let execute: (Action<Input, Output, Error>, Input) -> SignalProducer<Output, ActionError<Error>>
|
|
private let eventsObserver: Signal<Signal<Output, Error>.Event, Never>.Observer
|
|
private let disabledErrorsObserver: Signal<(), Never>.Observer
|
|
|
|
private let deinitToken: Lifetime.Token
|
|
|
|
/// The lifetime of the `Action`.
|
|
public let lifetime: Lifetime
|
|
|
|
/// A signal of all events generated from all units of work of the `Action`.
|
|
///
|
|
/// In other words, this sends every `Event` from every unit of work that the `Action`
|
|
/// executes.
|
|
public let events: Signal<Signal<Output, Error>.Event, Never>
|
|
|
|
/// A signal of all values generated from all units of work of the `Action`.
|
|
///
|
|
/// In other words, this sends every value from every unit of work that the `Action`
|
|
/// executes.
|
|
public let values: Signal<Output, Never>
|
|
|
|
/// A signal of all errors generated from all units of work of the `Action`.
|
|
///
|
|
/// In other words, this sends every error from every unit of work that the `Action`
|
|
/// executes.
|
|
public let errors: Signal<Error, Never>
|
|
|
|
/// A signal of all failed attempts to start a unit of work of the `Action`.
|
|
public let disabledErrors: Signal<(), Never>
|
|
|
|
/// A signal of all completed events generated from applications of the action.
|
|
///
|
|
/// In other words, this will send completed events from every signal generated
|
|
/// by each SignalProducer returned from apply().
|
|
public let completed: Signal<(), Never>
|
|
|
|
/// Whether the action is currently executing.
|
|
public let isExecuting: Property<Bool>
|
|
|
|
/// Whether the action is currently enabled.
|
|
public let isEnabled: Property<Bool>
|
|
|
|
/// Initializes an `Action` that would be conditionally enabled depending on its
|
|
/// state.
|
|
///
|
|
/// When the `Action` is asked to start the execution with an input value, a unit of
|
|
/// work — represented by a `SignalProducer` — would be created by invoking
|
|
/// `execute` with the latest state and the input value.
|
|
///
|
|
/// - note: `Action` guarantees that changes to `state` are observed in a
|
|
/// thread-safe way. Thus, the value passed to `isEnabled` will
|
|
/// always be identical to the value passed to `execute`, for each
|
|
/// application of the action.
|
|
///
|
|
/// - note: This initializer should only be used if you need to provide
|
|
/// custom input can also influence whether the action is enabled.
|
|
/// The various convenience initializers should cover most use cases.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A property to be the state of the `Action`.
|
|
/// - isEnabled: A predicate which determines the availability of the `Action`,
|
|
/// given the latest `Action` state.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to be
|
|
/// executed by the `Action`.
|
|
public init<State: PropertyProtocol>(state: State, enabledIf isEnabled: @escaping (State.Value) -> Bool, execute: @escaping (State.Value, Input) -> SignalProducer<Output, Error>) {
|
|
let isUserEnabled = isEnabled
|
|
|
|
(lifetime, deinitToken) = Lifetime.make()
|
|
|
|
// `Action` retains its state property.
|
|
lifetime.observeEnded { _ = state }
|
|
|
|
(events, eventsObserver) = Signal<Signal<Output, Error>.Event, Never>.pipe()
|
|
(disabledErrors, disabledErrorsObserver) = Signal<(), Never>.pipe()
|
|
|
|
values = events.compactMap { $0.value }
|
|
errors = events.compactMap { $0.error }
|
|
completed = events.compactMap { $0.isCompleted ? () : nil }
|
|
|
|
let actionState = MutableProperty(ActionState<State.Value>(isUserEnabled: true, isExecuting: false, value: state.value))
|
|
|
|
// `isEnabled` and `isExecuting` have their own backing so that when the observers
|
|
// of these synchronously affects the action state, the signal of the action state
|
|
// does not deadlock due to the recursion.
|
|
let isExecuting = MutableProperty(false)
|
|
self.isExecuting = Property(capturing: isExecuting)
|
|
let isEnabled = MutableProperty(actionState.value.isEnabled)
|
|
self.isEnabled = Property(capturing: isEnabled)
|
|
|
|
func modifyActionState<Result>(_ action: (inout ActionState<State.Value>) throws -> Result) rethrows -> Result {
|
|
return try actionState.begin { storage in
|
|
let oldState = storage.value
|
|
defer {
|
|
let newState = storage.value
|
|
if oldState.isEnabled != newState.isEnabled {
|
|
isEnabled.value = newState.isEnabled
|
|
}
|
|
if oldState.isExecuting != newState.isExecuting {
|
|
isExecuting.value = newState.isExecuting
|
|
}
|
|
}
|
|
return try storage.modify(action)
|
|
}
|
|
}
|
|
|
|
lifetime += state.producer.startWithValues { value in
|
|
modifyActionState { state in
|
|
state.value = value
|
|
state.isUserEnabled = isUserEnabled(value)
|
|
}
|
|
}
|
|
|
|
self.execute = { action, input in
|
|
return SignalProducer { observer, lifetime in
|
|
let latestState: State.Value? = modifyActionState { state in
|
|
guard state.isEnabled else {
|
|
return nil
|
|
}
|
|
|
|
state.isExecuting = true
|
|
return state.value
|
|
}
|
|
|
|
guard let state = latestState else {
|
|
observer.send(error: .disabled)
|
|
action.disabledErrorsObserver.send(value: ())
|
|
return
|
|
}
|
|
|
|
let interruptHandle = execute(state, input).start { event in
|
|
observer.send(event.mapError(ActionError.producerFailed))
|
|
action.eventsObserver.send(value: event)
|
|
}
|
|
|
|
lifetime.observeEnded {
|
|
interruptHandle.dispose()
|
|
modifyActionState { $0.isExecuting = false }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Initializes an `Action` that uses a property as its state.
|
|
///
|
|
/// When the `Action` is asked to start the execution, a unit of work — represented by
|
|
/// a `SignalProducer` — would be created by invoking `execute` with the latest value
|
|
/// of the state.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A property to be the state of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to
|
|
/// be executed by the `Action`.
|
|
public convenience init<P: PropertyProtocol>(state: P, execute: @escaping (P.Value, Input) -> SignalProducer<Output, Error>) {
|
|
self.init(state: state, enabledIf: { _ in true }, execute: execute)
|
|
}
|
|
|
|
/// Initializes an `Action` that would be conditionally enabled.
|
|
///
|
|
/// When the `Action` is asked to start the execution with an input value, a unit of
|
|
/// work — represented by a `SignalProducer` — would be created by invoking
|
|
/// `execute` with the input value.
|
|
///
|
|
/// - parameters:
|
|
/// - isEnabled: A property which determines the availability of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to be
|
|
/// executed by the `Action`.
|
|
public convenience init<P: PropertyProtocol>(enabledIf isEnabled: P, execute: @escaping (Input) -> SignalProducer<Output, Error>) where P.Value == Bool {
|
|
self.init(state: isEnabled, enabledIf: { $0 }) { _, input in
|
|
execute(input)
|
|
}
|
|
}
|
|
|
|
/// Initializes an `Action` that uses a property of optional as its state.
|
|
///
|
|
/// When the `Action` is asked to start executing, a unit of work (represented by
|
|
/// a `SignalProducer`) is created by invoking `execute` with the latest value
|
|
/// of the state and the `input` that was passed to `apply()`.
|
|
///
|
|
/// If the property holds a `nil`, the `Action` would be disabled until it is not
|
|
/// `nil`.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A property of optional to be the state of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to
|
|
/// be executed by the `Action`.
|
|
public convenience init<P: PropertyProtocol, T>(unwrapping state: P, execute: @escaping (T, Input) -> SignalProducer<Output, Error>) where P.Value == T? {
|
|
self.init(state: state, enabledIf: { $0 != nil }) { state, input in
|
|
execute(state!, input)
|
|
}
|
|
}
|
|
|
|
/// Initializes an `Action` that uses a `ValidatingProperty` as its state.
|
|
///
|
|
/// When the `Action` is asked to start executing, a unit of work (represented by
|
|
/// a `SignalProducer`) is created by invoking `execute` with the latest value
|
|
/// of the state and the `input` that was passed to `apply()`.
|
|
///
|
|
/// If the `ValidatingProperty` does not hold a valid value, the `Action` would be
|
|
/// disabled until it's valid.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A `ValidatingProperty` to be the state of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to
|
|
/// be executed by the `Action`.
|
|
public convenience init<T, E>(validated state: ValidatingProperty<T, E>, execute: @escaping (T, Input) -> SignalProducer<Output, Error>) {
|
|
self.init(unwrapping: state.result.map { $0.value }, execute: execute)
|
|
}
|
|
|
|
|
|
/// Initializes an `Action` that would always be enabled.
|
|
///
|
|
/// When the `Action` is asked to start the execution with an input value, a unit of
|
|
/// work — represented by a `SignalProducer` — would be created by invoking
|
|
/// `execute` with the input value.
|
|
///
|
|
/// - parameters:
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to be
|
|
/// executed by the `Action`.
|
|
public convenience init(execute: @escaping (Input) -> SignalProducer<Output, Error>) {
|
|
self.init(enabledIf: Property(value: true), execute: execute)
|
|
}
|
|
|
|
deinit {
|
|
eventsObserver.sendCompleted()
|
|
disabledErrorsObserver.sendCompleted()
|
|
}
|
|
|
|
/// Create a `SignalProducer` that would attempt to create and start a unit of work of
|
|
/// the `Action`. The `SignalProducer` would forward only events generated by the unit
|
|
/// of work it created.
|
|
///
|
|
/// If the execution attempt is failed, the producer would fail with
|
|
/// `ActionError.disabled`.
|
|
///
|
|
/// - parameters:
|
|
/// - input: A value to be used to create the unit of work.
|
|
///
|
|
/// - returns: A producer that forwards events generated by its started unit of work,
|
|
/// or emits `ActionError.disabled` if the execution attempt is failed.
|
|
public func apply(_ input: Input) -> SignalProducer<Output, ActionError<Error>> {
|
|
return execute(self, input)
|
|
}
|
|
}
|
|
|
|
extension Action: BindingTargetProvider {
|
|
public var bindingTarget: BindingTarget<Input> {
|
|
return BindingTarget(lifetime: lifetime) { [weak self] in self?.apply($0).start() }
|
|
}
|
|
}
|
|
|
|
extension Action where Input == Void {
|
|
/// Create a `SignalProducer` that would attempt to create and start a unit of work of
|
|
/// the `Action`. The `SignalProducer` would forward only events generated by the unit
|
|
/// of work it created.
|
|
///
|
|
/// If the execution attempt is failed, the producer would fail with
|
|
/// `ActionError.disabled`.
|
|
///
|
|
/// - returns: A producer that forwards events generated by its started unit of work,
|
|
/// or emits `ActionError.disabled` if the execution attempt is failed.
|
|
public func apply() -> SignalProducer<Output, ActionError<Error>> {
|
|
return apply(())
|
|
}
|
|
|
|
/// Initializes an `Action` that uses a property of optional as its state.
|
|
///
|
|
/// When the `Action` is asked to start the execution, a unit of work — represented by
|
|
/// a `SignalProducer` — would be created by invoking `execute` with the latest value
|
|
/// of the state.
|
|
///
|
|
/// If the property holds a `nil`, the `Action` would be disabled until it is not
|
|
/// `nil`.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A property of optional to be the state of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to
|
|
/// be executed by the `Action`.
|
|
public convenience init<P: PropertyProtocol, T>(unwrapping state: P, execute: @escaping (T) -> SignalProducer<Output, Error>) where P.Value == T? {
|
|
self.init(unwrapping: state) { state, _ in
|
|
execute(state)
|
|
}
|
|
}
|
|
|
|
/// Initializes an `Action` that uses a `ValidatingProperty` as its state.
|
|
///
|
|
/// When the `Action` is asked to start executing, a unit of work (represented by
|
|
/// a `SignalProducer`) is created by invoking `execute` with the latest value
|
|
/// of the state and the `input` that was passed to `apply()`.
|
|
///
|
|
/// If the `ValidatingProperty` does not hold a valid value, the `Action` would be
|
|
/// disabled until it's valid.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A `ValidatingProperty` to be the state of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to
|
|
/// be executed by the `Action`.
|
|
public convenience init<T, E>(validated state: ValidatingProperty<T, E>, execute: @escaping (T) -> SignalProducer<Output, Error>) {
|
|
self.init(validated: state) { state, _ in
|
|
execute(state)
|
|
}
|
|
}
|
|
|
|
/// Initializes an `Action` that uses a property as its state.
|
|
///
|
|
/// When the `Action` is asked to start the execution, a unit of work — represented by
|
|
/// a `SignalProducer` — would be created by invoking `execute` with the latest value
|
|
/// of the state.
|
|
///
|
|
/// - parameters:
|
|
/// - state: A property to be the state of the `Action`.
|
|
/// - execute: A closure that produces a unit of work, as `SignalProducer`, to
|
|
/// be executed by the `Action`.
|
|
public convenience init<P: PropertyProtocol, T>(state: P, execute: @escaping (T) -> SignalProducer<Output, Error>) where P.Value == T {
|
|
self.init(state: state) { state, _ in
|
|
execute(state)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `ActionError` represents the error that could be emitted by a unit of work of a
|
|
/// certain `Action`.
|
|
public enum ActionError<Error: Swift.Error>: Swift.Error {
|
|
/// The execution attempt was failed, since the `Action` was disabled.
|
|
case disabled
|
|
|
|
/// The unit of work emitted an error.
|
|
case producerFailed(Error)
|
|
}
|
|
|
|
extension ActionError where Error: Equatable {
|
|
public static func == (lhs: ActionError<Error>, rhs: ActionError<Error>) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case (.disabled, .disabled):
|
|
return true
|
|
|
|
case let (.producerFailed(left), .producerFailed(right)):
|
|
return left == right
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ActionError: Equatable where Error: Equatable {}
|
|
|