From 5a7be122ade1a54d34537ab07aea544d30e2a37f Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Tue, 20 Jan 2026 19:28:18 +0100 Subject: [PATCH] Migrate to Swift 6.2 --- Framework/Sources/StateMachine.swift | 109 +++++++++----------------- Framework/Sources/Transition.swift | 7 +- Package.swift | 2 +- Tests/Sources/StateMachineTests.swift | 66 ++++++++++------ 4 files changed, 83 insertions(+), 101 deletions(-) diff --git a/Framework/Sources/StateMachine.swift b/Framework/Sources/StateMachine.swift index 82b2f41..0b513fa 100644 --- a/Framework/Sources/StateMachine.swift +++ b/Framework/Sources/StateMachine.swift @@ -7,93 +7,58 @@ import Foundation -open class StateMachine { +public actor StateMachine { - public var enableLogging: Bool = false - public var currentState: State { - return { - workingQueue.sync { - return internalCurrentState - } - }() - } - - private var internalCurrentState: State - private var transitionsByEvent: [Event : [Transition]] = [:] + 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]] = [:] - 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) { - 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]? - 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)") } } diff --git a/Framework/Sources/Transition.swift b/Framework/Sources/Transition.swift index 47897f7..1abcc50 100644 --- a/Framework/Sources/Transition.swift +++ b/Framework/Sources/Transition.swift @@ -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 { +public struct Transition: Sendable { public let event: Event public let source: State diff --git a/Package.swift b/Package.swift index 65cccc7..481f74c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.11 +// swift-tools-version: 6.2 import PackageDescription diff --git a/Tests/Sources/StateMachineTests.swift b/Tests/Sources/StateMachineTests.swift index d62a114..b55ae89 100644 --- a/Tests/Sources/StateMachineTests.swift +++ b/Tests/Sources/StateMachineTests.swift @@ -13,12 +13,12 @@ class StateMachineTests: XCTestCase { typealias TransitionDefault = Transition typealias StateMachineDefault = StateMachine - enum EventType { + enum EventType: Sendable { case e1 case e2 } - enum StateType { + enum StateType: Sendable { case idle case started case running @@ -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) } }