Changes
8 changed files (+157/-45)
-
macos/RoonKit/Sources/RoonKit/Connection.swift > macos/RoonKit/Sources/RoonKit/Connection/Connection.swift
-
@@ -18,7 +18,7 @@ import Networkimport OSLog import Observation public actor Connection { public actor Connection: Connectable, Communicatable { private let logger = Logger(subsystem: subsystem, category: "Connection") public let serverID: String
-
@@ -250,13 +250,6 @@ conn.cancel()} } /// A stream of incoming messages. Roon API uses WebSocket for most of the operation, /// even including request-response methods. Due to this nature, caller must check /// "Request-Id" header field to determine if a message is a response for the request. /// /// The stream will end when the underlying connection was closed. /// /// This is low-level API. Only use for continuous reading, such as change subscription. public var messages: AsyncStream<Moo> { let id = messageContinuationID messageContinuationID += 1
-
@@ -271,22 +264,8 @@ }} } public enum MessageReadError: Error { case connectionClosed case timeout } /// Send a MOO message to connected Roon server and returns a response for the message. /// /// This function sets "Request-Id" header if the message does not have one. /// Unless you have a specific reason, let this function set it so the ID will be unique among other requests. /// /// Throws when send failed or connection closed during the read. /// /// If you set `timeout` parameter and no response was made during the duration, `MessageReadError.timeout` /// error will be thrown. public func request( _ msg: consuming Moo, private func request( msg: consuming Moo, timeout: ContinuousClock.Instant.Duration? = nil ) async throws -> Moo { let requestID: RequestId
-
@@ -326,14 +305,17 @@return try await read.value } public enum MessageWriteError: Error { case connectionNotReady public func request(_ msg: consuming Moo) async throws -> Moo { return try await request(msg: msg) } public func request( _ msg: consuming Moo, timeout: ContinuousClock.Instant.Duration ) async throws -> Moo { return try await request(msg: msg, timeout: timeout) } /// Sends a MOO message to the connected Roon server, then returns. /// This function does not waits for a response message. /// /// Throws when a network error occured during send operation. public func send(_ msg: consuming Moo) async throws { let req = String(msg).data(using: .utf8)!
-
@@ -365,12 +347,12 @@ }} extension NWConnection { enum ConnectionNotReadyError: Error { fileprivate enum ConnectionNotReadyError: Error { case failed(NWError) case cancelled } func ready() async throws { fileprivate func ready() async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, any Error>) in self.stateUpdateHandler = { state in
-
@@ -391,7 +373,9 @@ }} } func receiveMessage() async throws -> (Data?, ContentContext?, Bool) { fileprivate func receiveMessage() async throws -> ( Data?, ContentContext?, Bool ) { return try await withCheckedThrowingContinuation { cont in self.receiveMessage { (data, context, received, error) in if let error = error {
-
@@ -404,7 +388,7 @@ }} } func send( fileprivate func send( _ data: Data, contentContext: ContentContext = ContentContext.defaultMessage ) async throws {
-
-
-
@@ -0,0 +1,60 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 public enum MessageReadError: Error { case connectionClosed case timeout } public enum MessageWriteError: Error { case connectionNotReady } public protocol Communicatable: Actor { /// A stream of incoming messages. Roon API uses WebSocket for most of the operation, /// even including request-response methods. Due to this nature, caller must check /// "Request-Id" header field to determine if a message is a response for the request. /// /// The stream will end when the underlying connection was closed. /// /// This is low-level API. Only use for continuous reading, such as change subscription. var messages: AsyncStream<Moo> { get } /// Send a MOO message to connected Roon server and returns a response for the message. /// /// This function sets "Request-Id" header if the message does not have one. /// Unless you have a specific reason, let this function set it so the ID will be unique among other requests. /// /// Throws when send failed or connection closed during the read. func request(_ msg: consuming Moo) async throws -> Moo /// Send a MOO message to connected Roon server and returns a response for the message. /// /// This function sets "Request-Id" header if the message does not have one. /// Unless you have a specific reason, let this function set it so the ID will be unique among other requests. /// /// Throws when send failed or connection closed during the read. /// /// If no response was made when the `timeout` elapsed, `MessageReadError.timeout` error will be thrown. func request(_ msg: consuming Moo, timeout: ContinuousClock.Instant.Duration) async throws -> Moo /// Sends a MOO message to the connected Roon server, then returns. /// This function does not waits for a response message. /// /// Throws when a network error occured during send operation. func send(_ msg: consuming Moo) async throws }
-
-
-
@@ -0,0 +1,33 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 public protocol Connectable: Actor { /// Roon server ID of the target Roon Core. var serverID: String { get } /// Host, mostly IPv4 address of the target Roon Core. var host: String { get } /// HTTP port to use for WebSocket connection to the target Roon Core. var port: UInt16 { get } /// Open a new connection if the current connection is closed. /// Throws when the current connection is already open. func connect() async throws /// Close the current connection. func disconnect() }
-
-
-
@@ -20,7 +20,7 @@ import SwiftUIenum ConnectionState { case loading case open(Connection) case open(Communicatable & Connectable, (String, UInt16)) case closed case error(any Error) }
-
@@ -49,7 +49,7 @@ var state: ConnectionState = .closedvar disconnectable: Bool { switch state { case .open(_): case .open(_, _): true default: false
-
@@ -136,8 +136,8 @@ port: port,ext: ext ) state = .open(conn) address = (host, port) state = .open(conn, (host, port)) connectedServerID = serverID serverIp = host
-
@@ -150,7 +150,7 @@ }func disconnect() async { switch state { case .open(let conn): case .open(let conn, _): await conn.disconnect() clearCache() state = .closed
-
-
-
@@ -0,0 +1,34 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 import Foundation import Observation import RoonKit @Observable final class ImageURLBuilderDataModel { private var host: String private var port: UInt16 init(host: String, port: UInt16) { self.host = host self.port = port } public func build(req: ImageService.GetRequest) -> URL? { URL(roonImage: req, host: host, port: port) } }
-
-
-
@@ -19,7 +19,7 @@ import SwiftUI@Observable final class ZoneDataModel { let conn: Connection let conn: Communicatable private var zoneID: TransportService.Zone.ID? = nil private var _zones: [TransportService.Zone.ID: TransportService.Zone] = [:]
-
@@ -50,7 +50,7 @@set { zoneID = newValue?.id } } init(conn: Connection) { init(conn: Communicatable) { self.conn = conn }
-
-
-
@@ -21,7 +21,7 @@ import osenum MainViewState { case loading case found(RoonKit.Connection) case found(Communicatable) case error(any Error) }
-
@@ -38,9 +38,10 @@ switch conn.state {case .loading, .closed: // TODO: Create view for closed state ProgressView() case .open(let conn): case .open(let conn, let (host, port)): ConnectedView(conn: conn) .environment(self.conn) .environment(ImageURLBuilderDataModel(host: host, port: port)) case .error(RoonKit.ServerLookupError.notFound): ContentUnavailableView { Label(
-
@@ -73,7 +74,7 @@struct ConnectedView: View { @State private var model: ZoneDataModel init(conn: Connection) { init(conn: Communicatable & Connectable) { model = ZoneDataModel(conn: conn) }
-
-
-
@@ -18,14 +18,14 @@ import RoonKitimport SwiftUI struct Artwork: View { @Environment(ConnectionDataModel.self) var conn @Environment(ImageURLBuilderDataModel.self) var builder let imageKey: String let width: CGFloat let height: CGFloat private var url: URL? { conn.imageURL( builder.build( req: .init( key: imageKey, format: .png,
-