parent
89d50e6e94
commit
b0359f127b
@ -0,0 +1,69 @@ |
|||||||
|
import Foundation |
||||||
|
|
||||||
|
/// Represents the connection lifecycle of the remote client. |
||||||
|
/// Transitions are validated by `canTransition(to:)` to enforce a strict state machine. |
||||||
|
nonisolated enum ConnectionState: Equatable, Sendable { |
||||||
|
case disconnected |
||||||
|
case discovering |
||||||
|
case foundHost(String) |
||||||
|
case downloadingDB |
||||||
|
case connectingCommandChannel |
||||||
|
case connected(String) |
||||||
|
case connectionLost(String) |
||||||
|
|
||||||
|
/// Human-readable status message for display in the UI. |
||||||
|
var userMessage: String? { |
||||||
|
switch self { |
||||||
|
case .disconnected: |
||||||
|
nil |
||||||
|
case .discovering: |
||||||
|
"Searching for hosts..." |
||||||
|
case .foundHost(let host): |
||||||
|
"Found \(host)" |
||||||
|
case .downloadingDB: |
||||||
|
"Downloading library..." |
||||||
|
case .connectingCommandChannel: |
||||||
|
"Connecting..." |
||||||
|
case .connected(let host): |
||||||
|
"Connected to \(host)" |
||||||
|
case .connectionLost(let reason): |
||||||
|
"Connection lost — \(reason)" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Whether the client is fully connected. |
||||||
|
var isConnected: Bool { |
||||||
|
if case .connected = self { return true } |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
/// Validates whether transitioning from the current state to `next` is allowed. |
||||||
|
/// |
||||||
|
/// Valid forward transitions follow the connection lifecycle: |
||||||
|
/// disconnected → discovering → foundHost → downloadingDB → connectingCommandChannel → connected → connectionLost → discovering |
||||||
|
/// |
||||||
|
/// Any state may transition to `.disconnected` (the user can always disconnect). |
||||||
|
func canTransition(to next: ConnectionState) -> Bool { |
||||||
|
// Any state can go to disconnected |
||||||
|
if case .disconnected = next { return true } |
||||||
|
|
||||||
|
switch self { |
||||||
|
case .disconnected: |
||||||
|
if case .discovering = next { return true } |
||||||
|
case .discovering: |
||||||
|
if case .foundHost = next { return true } |
||||||
|
case .foundHost: |
||||||
|
if case .downloadingDB = next { return true } |
||||||
|
case .downloadingDB: |
||||||
|
if case .connectingCommandChannel = next { return true } |
||||||
|
case .connectingCommandChannel: |
||||||
|
if case .connected = next { return true } |
||||||
|
case .connected: |
||||||
|
if case .connectionLost = next { return true } |
||||||
|
case .connectionLost: |
||||||
|
if case .discovering = next { return true } |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
import Testing |
||||||
|
import Foundation |
||||||
|
@testable import Music |
||||||
|
|
||||||
|
struct ConnectionStateTests { |
||||||
|
|
||||||
|
// Verifies that each valid forward transition is allowed. |
||||||
|
@Test func validTransitionsSucceed() { |
||||||
|
#expect(ConnectionState.disconnected.canTransition(to: .discovering) == true) |
||||||
|
#expect(ConnectionState.discovering.canTransition(to: .foundHost("Mac Mini")) == true) |
||||||
|
#expect(ConnectionState.foundHost("Mac Mini").canTransition(to: .downloadingDB) == true) |
||||||
|
#expect(ConnectionState.downloadingDB.canTransition(to: .connectingCommandChannel) == true) |
||||||
|
#expect(ConnectionState.connectingCommandChannel.canTransition(to: .connected("Mac Mini")) == true) |
||||||
|
#expect(ConnectionState.connected("Mac Mini").canTransition(to: .connectionLost("Network changed")) == true) |
||||||
|
#expect(ConnectionState.connectionLost("Network changed").canTransition(to: .discovering) == true) |
||||||
|
} |
||||||
|
|
||||||
|
// Verifies that skipping states is rejected. |
||||||
|
@Test func invalidTransitionsRejected() { |
||||||
|
#expect(ConnectionState.disconnected.canTransition(to: .connected("Mac Mini")) == false) |
||||||
|
#expect(ConnectionState.discovering.canTransition(to: .connected("Mac Mini")) == false) |
||||||
|
#expect(ConnectionState.connected("Mac Mini").canTransition(to: .discovering) == false) |
||||||
|
} |
||||||
|
|
||||||
|
// Any state can transition to disconnected (user can always disconnect). |
||||||
|
@Test func anyStateCanDisconnect() { |
||||||
|
#expect(ConnectionState.discovering.canTransition(to: .disconnected) == true) |
||||||
|
#expect(ConnectionState.foundHost("X").canTransition(to: .disconnected) == true) |
||||||
|
#expect(ConnectionState.downloadingDB.canTransition(to: .disconnected) == true) |
||||||
|
#expect(ConnectionState.connectingCommandChannel.canTransition(to: .disconnected) == true) |
||||||
|
#expect(ConnectionState.connected("X").canTransition(to: .disconnected) == true) |
||||||
|
} |
||||||
|
|
||||||
|
// Verifies the human-readable status message for each state. |
||||||
|
@Test func userVisibleDescriptions() { |
||||||
|
#expect(ConnectionState.disconnected.userMessage == nil) |
||||||
|
#expect(ConnectionState.discovering.userMessage == "Searching for hosts...") |
||||||
|
#expect(ConnectionState.foundHost("Mac Mini").userMessage == "Found Mac Mini") |
||||||
|
#expect(ConnectionState.downloadingDB.userMessage == "Downloading library...") |
||||||
|
#expect(ConnectionState.connectingCommandChannel.userMessage == "Connecting...") |
||||||
|
#expect(ConnectionState.connected("Mac Mini").userMessage == "Connected to Mac Mini") |
||||||
|
#expect(ConnectionState.connectionLost("Network changed").userMessage == "Connection lost — Network changed") |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue