Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 37 additions & 72 deletions Framework/Sources/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,93 +7,58 @@

import Foundation

open class StateMachine<State: Hashable, Event: Hashable> {
public actor StateMachine<State: Hashable & Sendable, Event: Hashable & Sendable> {

public var enableLogging: Bool = false
public var currentState: State {
return {
workingQueue.sync {
return internalCurrentState
}
}()
}

private var internalCurrentState: State
private var transitionsByEvent: [Event : [Transition<State, Event>]] = [:]
nonisolated(unsafe) public var enableLogging: Bool = false
public private(set) var currentState: State

private let lockQueue: DispatchQueue
private let workingQueue: DispatchQueue
private let callbackQueue: DispatchQueue
private var transitionsByEvent: [Event: [Transition<State, Event>]] = [:]

public init(initialState: State, callbackQueue: DispatchQueue? = nil) {
self.internalCurrentState = initialState
self.lockQueue = DispatchQueue(label: "com.albertodebortoli.statemachine.queue.lock")
self.workingQueue = DispatchQueue(label: "com.albertodebortoli.statemachine.queue.working")
self.callbackQueue = callbackQueue ?? .main
public init(initialState: State) {
self.currentState = initialState
}

public func add(transition: Transition<State, Event>) {
lockQueue.sync {
if let transitions = self.transitionsByEvent[transition.event] {
if (transitions.filter { return $0.source == transition.source }.count > 0) {
assertionFailure("Transition with event '\(transition.event)' and source '\(transition.source)' already existing.")
}
self.transitionsByEvent[transition.event]?.append(transition)
} else {
self.transitionsByEvent[transition.event] = [transition]
if let transitions = transitionsByEvent[transition.event] {
if transitions.contains(where: { $0.source == transition.source }) {
assertionFailure("Transition with event '\(transition.event)' and source '\(transition.source)' already existing.")
}
transitionsByEvent[transition.event]?.append(transition)
} else {
transitionsByEvent[transition.event] = [transition]
}
}

public func process(event: Event, execution: (() -> Void)? = nil, callback: TransitionBlock? = nil) {
var transitions: [Transition<State, Event>]?
lockQueue.sync {
transitions = self.transitionsByEvent[event]
}
public func process(event: Event) -> TransitionResult {
let transitions = transitionsByEvent[event]
let performableTransitions = transitions?.filter { $0.source == currentState } ?? []

workingQueue.async {
let performableTransitions = transitions?.filter { return $0.source == self.internalCurrentState } ?? []

if performableTransitions.count == 0 {
self.callbackQueue.async {
callback?(.failure)
}
return
}

assert(performableTransitions.count == 1, "Found multiple transitions with event '\(event)' and source '\(self.internalCurrentState)'.")

let transition = performableTransitions.first!

self.log(message: "Processing event '\(event)' from '\(self.internalCurrentState)'")
self.callbackQueue.async {
transition.executePreBlock()
}

self.log(message: "Processed pre condition for event '\(event)' from '\(transition.source)' to '\(transition.destination)'")

self.callbackQueue.async {
execution?()
}

let previousState = self.internalCurrentState
self.internalCurrentState = transition.destination

self.log(message: "Processed state change from '\(previousState)' to '\(transition.destination)'")
self.callbackQueue.async {
transition.executePostBlock()
}

self.log(message: "Processed post condition for event '\(event)' from '\(transition.source)' to '\(transition.destination)'")

self.callbackQueue.async {
callback?(.success)
}
if performableTransitions.isEmpty {
return .failure
}

assert(performableTransitions.count == 1, "Found multiple transitions with event '\(event)' and source '\(currentState)'.")

let transition = performableTransitions.first!

log(message: "Processing event '\(event)' from '\(currentState)'")
transition.executePreBlock()

log(message: "Processed pre condition for event '\(event)' from '\(transition.source)' to '\(transition.destination)'")

let previousState = currentState
currentState = transition.destination

log(message: "Processed state change from '\(previousState)' to '\(transition.destination)'")
transition.executePostBlock()

log(message: "Processed post condition for event '\(event)' from '\(transition.source)' to '\(transition.destination)'")

return .success
}

private func log(message: String) {
if self.enableLogging {
if enableLogging {
print("[Stateful 🦜] \(message)")
}
}
Expand Down
7 changes: 3 additions & 4 deletions Framework/Sources/Transition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

import Foundation

public enum TransitionResult {
public enum TransitionResult: Sendable {
case success
case failure
}

public typealias ExecutionBlock = (() -> Void)
public typealias TransitionBlock = ((TransitionResult) -> Void)
public typealias ExecutionBlock = @Sendable () -> Void

public struct Transition<State, Event> {
public struct Transition<State: Sendable, Event: Sendable>: Sendable {

public let event: Event
public let source: State
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.11
// swift-tools-version: 6.2

import PackageDescription

Expand Down
66 changes: 42 additions & 24 deletions Tests/Sources/StateMachineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ class StateMachineTests: XCTestCase {
typealias TransitionDefault = Transition<StateType, EventType>
typealias StateMachineDefault = StateMachine<StateType, EventType>

enum EventType {
enum EventType: Sendable {
case e1
case e2
}

enum StateType {
enum StateType: Sendable {
case idle
case started
case running
Expand All @@ -38,38 +38,56 @@ class StateMachineTests: XCTestCase {
super.tearDown()
}

func test_Creation() {
XCTAssertEqual(stateMachine.currentState, .idle)
func test_Creation() async {
let state = await stateMachine.currentState
XCTAssertEqual(state, .idle)
}

func test_SingleTransition() {
stateMachine.process(event: .e1)
XCTAssertEqual(stateMachine.currentState, .idle)
func test_SingleTransition() async {
var result = await stateMachine.process(event: .e1)
XCTAssertEqual(result, .failure)
var state = await stateMachine.currentState
XCTAssertEqual(state, .idle)

let transition = TransitionDefault(with: .e1, from: .idle, to: .started)
stateMachine.add(transition: transition)
stateMachine.process(event: .e1)
XCTAssertEqual(stateMachine.currentState, .started)
await stateMachine.add(transition: transition)
result = await stateMachine.process(event: .e1)
XCTAssertEqual(result, .success)
state = await stateMachine.currentState
XCTAssertEqual(state, .started)
}

func test_MultipleTransistions() {
stateMachine.process(event: .e1)
XCTAssertEqual(stateMachine.currentState, .idle)
func test_MultipleTransistions() async {
var result = await stateMachine.process(event: .e1)
XCTAssertEqual(result, .failure)
var state = await stateMachine.currentState
XCTAssertEqual(state, .idle)

let transition1 = TransitionDefault(with: .e1, from: .idle, to: .started)
stateMachine.add(transition: transition1)
await stateMachine.add(transition: transition1)
let transition2 = TransitionDefault(with: .e2, from: .started, to: .idle)
stateMachine.add(transition: transition2)
await stateMachine.add(transition: transition2)
let transition3 = TransitionDefault(with: .e1, from: .started, to: .idle)
stateMachine.add(transition: transition3)
await stateMachine.add(transition: transition3)

stateMachine.process(event: .e1)
XCTAssertEqual(stateMachine.currentState, .started)
stateMachine.process(event: .e2)
XCTAssertEqual(stateMachine.currentState, .idle)
stateMachine.process(event: .e1)
XCTAssertEqual(stateMachine.currentState, .started)
stateMachine.process(event: .e1)
XCTAssertEqual(stateMachine.currentState, .idle)
result = await stateMachine.process(event: .e1)
XCTAssertEqual(result, .success)
state = await stateMachine.currentState
XCTAssertEqual(state, .started)

result = await stateMachine.process(event: .e2)
XCTAssertEqual(result, .success)
state = await stateMachine.currentState
XCTAssertEqual(state, .idle)

result = await stateMachine.process(event: .e1)
XCTAssertEqual(result, .success)
state = await stateMachine.currentState
XCTAssertEqual(state, .started)

result = await stateMachine.process(event: .e1)
XCTAssertEqual(result, .success)
state = await stateMachine.currentState
XCTAssertEqual(state, .idle)
}
}
Loading