|
// |
|
// StateMachine.swift |
|
// StateMachine |
|
// |
|
|
|
import Foundation |
|
|
|
protocol EventType: Hashable {} |
|
protocol StateType: Equatable {} |
|
protocol ContextType {} |
|
|
|
class Action<E: EventType, C: ContextType> { |
|
public var exec: (E, C?) -> Void { |
|
get { |
|
if let callback = self.callback { |
|
return callback |
|
} else { |
|
return self.main(event:context:) |
|
} |
|
} |
|
} |
|
|
|
public let queue: DispatchQueue? |
|
private let callback: ((E, C?) -> Void)? |
|
|
|
init(queue: DispatchQueue? = nil, callback: ((E, C?) -> Void)? = nil) { |
|
self.callback = callback |
|
self.queue = queue |
|
} |
|
|
|
func main(event: E, context: C?) {} |
|
} |
|
|
|
class Transition<S: StateType, E: EventType, C: ContextType> { |
|
public let target: S? |
|
public let actions: [Action<E, C>] |
|
public let condition: (E, C?) -> Bool |
|
|
|
init( |
|
target: S?, |
|
actions: [Action<E, C>] = [], |
|
condition: @escaping (E, C?) -> Bool = { _, _ in true } |
|
) { |
|
self.target = target |
|
self.actions = actions |
|
self.condition = condition |
|
} |
|
} |
|
|
|
class State<S: StateType, E: EventType, C: ContextType> { |
|
public let id: S |
|
public let on: [E: [Transition<S, E, C>]]? |
|
public let enter: [Action<E, C>] |
|
public let exit: [Action<E, C>] |
|
|
|
init( |
|
id: S, |
|
on: [E: [Transition<S, E, C>]]? = [:], |
|
enter: [Action<E, C>] = [], |
|
exit: [Action<E, C>] = [] |
|
) { |
|
self.id = id |
|
self.on = on |
|
self.enter = enter |
|
self.exit = exit |
|
} |
|
} |
|
|
|
struct TransitionResult<S: StateType, E: EventType, C: ContextType> { |
|
public let state: State<S, E, C> |
|
public let actions: [Action<E, C>]? |
|
public let context: C? |
|
public let changed: Bool |
|
} |
|
|
|
class Machine<S: StateType, E: EventType, C: ContextType> { |
|
public let initial: State<S, E, C> |
|
public let states: [State<S, E, C>] |
|
public let context: C? |
|
|
|
convenience init( |
|
initialType: S, |
|
states: [State<S, E, C>], |
|
context: C? = nil |
|
) { |
|
let initial = states.first(where: { $0.id == initialType }) |
|
self.init(initial: initial!, states: states, context: context) |
|
} |
|
|
|
init( |
|
initial: State<S, E, C>, |
|
states: [State<S, E, C>], |
|
context: C? = nil |
|
) { |
|
self.initial = initial |
|
self.states = states |
|
self.context = context |
|
} |
|
|
|
func transition(currentState: State<S, E, C>, event: E) -> TransitionResult<S, E, C> { |
|
guard let current = states.first(where: {$0.id == currentState.id}) else { |
|
print("State '\(currentState)' not found on machine") |
|
return unchanged(state: currentState) |
|
} |
|
|
|
guard let on = current.on, let transitions = on[event] else { |
|
return unchanged(state: current) |
|
} |
|
|
|
let result = transitions |
|
.filter{ $0.condition(event, context) } |
|
.map{ (transition: Transition<S, E, C>) -> TransitionResult<S, E, C> in |
|
guard let target = transition.target == nil ? current.id : transition.target else { |
|
return unchanged(state: current) |
|
} |
|
guard let next = states.first(where: {$0.id == target}) else { |
|
return unchanged(state: current) |
|
} |
|
|
|
var actions: [Action<E, C>] = [] |
|
actions.append(contentsOf: current.exit) |
|
actions.append(contentsOf: transition.actions) |
|
actions.append(contentsOf: next.enter) |
|
|
|
return TransitionResult<S, E, C>( |
|
state: next, |
|
actions: actions, |
|
context: self.context, |
|
changed: target != current.id |
|
) |
|
} |
|
.first |
|
|
|
guard let transition = result else { |
|
return unchanged(state: current) |
|
} |
|
|
|
return transition |
|
} |
|
|
|
private func unchanged(state: State<S, E, C>) -> TransitionResult<S, E, C> { |
|
return TransitionResult(state: state, actions: nil, context: self.context, changed: false) |
|
} |
|
} |
|
|
|
class Service<S: StateType, E: EventType, C: ContextType> { |
|
private enum Status { |
|
case noStarted |
|
case running |
|
case stopped |
|
} |
|
|
|
private let machine: Machine<S, E, C> |
|
private var current: TransitionResult<S, E, C> |
|
private var listeners: [(TransitionResult<S, E, C>) -> Void] = [] |
|
private var status: Status = .noStarted |
|
|
|
public var state: State<S, E, C> { |
|
get { |
|
return current.state |
|
} |
|
} |
|
|
|
convenience init( |
|
initialType: S, |
|
states: [State<S, E, C>], |
|
context: C? = nil |
|
) { |
|
let initial = states.first(where: { $0.id == initialType }) |
|
self.init(initial: initial!, states: states, context: context) |
|
} |
|
|
|
convenience init( |
|
initial: State<S, E, C>, |
|
states: [State<S, E, C>], |
|
context: C? = nil |
|
) { |
|
self.init(machine: Machine<S, E, C>(initial: initial, states: states, context: context)) |
|
} |
|
|
|
init(machine: Machine<S, E, C>) { |
|
self.machine = machine |
|
self.current = TransitionResult( |
|
state: machine.initial, |
|
actions: machine.initial.enter, |
|
context: machine.context, |
|
changed: false |
|
) |
|
} |
|
|
|
@discardableResult |
|
public func subscribe(_ listener: @escaping (TransitionResult<S, E, C>) -> Void) -> () -> Void { |
|
listeners.append(listener) |
|
let itemIndex = listeners.count - 1 |
|
listener(current) |
|
|
|
return { [self] in |
|
if (itemIndex < listeners.count) { |
|
self.listeners.remove(at: itemIndex) |
|
} |
|
} |
|
} |
|
|
|
@discardableResult |
|
public func start(initEvent event: E) -> Service { |
|
status = .running |
|
current.actions?.forEach { action in |
|
guard let queue = action.queue else { |
|
action.exec(event, current.context) |
|
return |
|
} |
|
|
|
queue.async { [self] in |
|
action.exec(event, current.context) |
|
} |
|
} |
|
return self; |
|
} |
|
|
|
@discardableResult |
|
public func stop() -> Service { |
|
status = .stopped |
|
listeners.removeAll() |
|
return self; |
|
} |
|
|
|
public func send(event: E) { |
|
guard status == .running else { fatalError("Sending event without calling .start()") } |
|
current = machine.transition(currentState: current.state, event: event) |
|
|
|
var notified: [(TransitionResult<S, E, C>) -> Void] = [] |
|
current.actions?.forEach{ action in |
|
guard let queue = action.queue else { |
|
action.exec(event, current.context) |
|
return |
|
} |
|
|
|
queue.async { [self] in |
|
action.exec(event, current.context) |
|
} |
|
} |
|
notified.append(contentsOf: listeners) |
|
notified.forEach{ $0(current) } |
|
} |
|
} |
|
|
|
class AsyncService<S: StateType, E: EventType, C: ContextType> { |
|
private enum Status { |
|
case noStarted |
|
case running |
|
case stopped |
|
} |
|
|
|
class ListenerWrapper { |
|
public let callback: (TransitionResult<S, E, C>) -> Void |
|
public let queue: DispatchQueue |
|
|
|
init(callback: @escaping (TransitionResult<S, E, C>) -> Void, queue: DispatchQueue) { |
|
self.callback = callback |
|
self.queue = queue |
|
} |
|
} |
|
|
|
class ActionOperation: Operation { |
|
private let semaphore = DispatchSemaphore(value: 0) |
|
private let action: Action<E, C> |
|
private let event: E |
|
private let context: C? |
|
|
|
init(action: Action<E, C>, event: E, context: C?) { |
|
self.action = action |
|
self.event = event |
|
self.context = context |
|
} |
|
|
|
override func main() { |
|
guard let queue = action.queue else { |
|
action.exec(event, context) |
|
semaphore.signal() |
|
return |
|
} |
|
|
|
queue.async { [self] in |
|
action.exec(event, context) |
|
semaphore.signal() |
|
} |
|
|
|
semaphore.wait() |
|
} |
|
} |
|
|
|
private let machine: Machine<S, E, C> |
|
private var current: TransitionResult<S, E, C> |
|
|
|
private var listeners: [ListenerWrapper] = [] |
|
private var status: Status = .noStarted |
|
|
|
private let workQueue = DispatchQueue(label: "com.vavavoom.sdk.statemachine") |
|
private let actionQueue: OperationQueue = { |
|
var queue = OperationQueue() |
|
queue.name = "com.vavavoom.sdk.statemachine.actions" |
|
return queue |
|
}() |
|
|
|
convenience init( |
|
initialType: S, |
|
states: [State<S, E, C>], |
|
context: C? = nil |
|
) { |
|
let initial = states.first(where: { $0.id == initialType }) |
|
self.init(initial: initial!, states: states, context: context) |
|
} |
|
|
|
convenience init( |
|
initial: State<S, E, C>, |
|
states: [State<S, E, C>], |
|
context: C? = nil |
|
) { |
|
self.init(machine: Machine<S, E, C>(initial: initial, states: states, context: context)) |
|
} |
|
|
|
init(machine: Machine<S, E, C>) { |
|
self.machine = machine |
|
|
|
self.current = TransitionResult( |
|
state: machine.initial, |
|
actions: machine.initial.enter, |
|
context: machine.context, |
|
changed: false |
|
) |
|
} |
|
|
|
@discardableResult |
|
public func start(initEvent event: E) -> Self { |
|
guard status != .running else { fatalError("Service is already runing") } |
|
status = .running |
|
workQueue.async { [self] in |
|
self.runActions( |
|
actions: current.actions, |
|
event: event, |
|
context: current.context |
|
) |
|
} |
|
|
|
return self; |
|
} |
|
|
|
@discardableResult |
|
public func stop() -> Self { |
|
status = .stopped |
|
listeners.removeAll() |
|
return self; |
|
} |
|
|
|
public func send(event: E) { |
|
guard status == .running else { fatalError("Sending event without calling .start()") } |
|
workQueue.async { [self] in |
|
current = machine.transition(currentState: current.state, event: event) |
|
|
|
self.runActions( |
|
actions: current.actions, |
|
event: event, |
|
context: current.context |
|
) |
|
|
|
var notified: [ListenerWrapper] = [] |
|
notified.append(contentsOf: listeners) |
|
notified.forEach{ listener in |
|
listener.queue.async { |
|
listener.callback(current) |
|
} |
|
} |
|
} |
|
} |
|
|
|
@discardableResult |
|
public func subscribe( |
|
_ listener: @escaping (TransitionResult<S, E, C>) -> Void, |
|
queue: DispatchQueue = .main |
|
) -> () -> Void { |
|
listeners.append(ListenerWrapper(callback: listener, queue: queue)) |
|
let itemIndex = listeners.count - 1 |
|
listener(current) |
|
|
|
return { [self] in |
|
if (itemIndex < listeners.count) { |
|
self.listeners.remove(at: itemIndex) |
|
} |
|
} |
|
} |
|
|
|
private func runActions(actions: [Action<E, C>]?, event: E, context: C?) { |
|
guard let actions = actions, actions.count > 0 else { |
|
return |
|
} |
|
|
|
let operations = actions.map { action in |
|
return ActionOperation( |
|
action:action, |
|
event: event, |
|
context: context |
|
) |
|
} |
|
self.actionQueue.addOperations(operations, waitUntilFinished: true) |
|
} |
|
} |