mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
169 lines
5.3 KiB
Swift
169 lines
5.3 KiB
Swift
import Foundation
|
|
|
|
/// Protocol for WebSocket operations to enable testing.
|
|
/// Defines the interface for WebSocket connections and messaging.
|
|
@MainActor
|
|
protocol WebSocketProtocol: AnyObject {
|
|
var delegate: WebSocketDelegate? { get set }
|
|
|
|
func connect(to url: URL, with headers: [String: String]) async throws
|
|
func send(_ message: WebSocketMessage) async throws
|
|
func sendPing() async throws
|
|
func disconnect(with code: URLSessionWebSocketTask.CloseCode, reason: Data?)
|
|
}
|
|
|
|
/// WebSocket message types.
|
|
/// Represents different types of messages that can be sent over WebSocket.
|
|
enum WebSocketMessage {
|
|
case string(String)
|
|
case data(Data)
|
|
}
|
|
|
|
/// Delegate protocol for WebSocket events.
|
|
/// Provides callbacks for connection state changes and message reception.
|
|
@MainActor
|
|
protocol WebSocketDelegate: AnyObject {
|
|
func webSocketDidConnect(_ webSocket: WebSocketProtocol)
|
|
func webSocket(_ webSocket: WebSocketProtocol, didReceiveMessage message: WebSocketMessage)
|
|
func webSocket(_ webSocket: WebSocketProtocol, didFailWithError error: Error)
|
|
func webSocketDidDisconnect(
|
|
_ webSocket: WebSocketProtocol,
|
|
closeCode: URLSessionWebSocketTask.CloseCode,
|
|
reason: Data?
|
|
)
|
|
}
|
|
|
|
/// Real implementation of WebSocketProtocol using URLSessionWebSocketTask.
|
|
/// Provides WebSocket functionality using iOS native URLSession APIs.
|
|
@MainActor
|
|
class URLSessionWebSocket: NSObject, WebSocketProtocol {
|
|
weak var delegate: WebSocketDelegate?
|
|
private var webSocketTask: URLSessionWebSocketTask?
|
|
private var session: URLSession?
|
|
private var isReceiving = false
|
|
|
|
override init() {
|
|
super.init()
|
|
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
|
|
}
|
|
|
|
func connect(to url: URL, with headers: [String: String]) async throws {
|
|
var request = URLRequest(url: url)
|
|
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
|
|
|
webSocketTask = session?.webSocketTask(with: request)
|
|
webSocketTask?.resume()
|
|
|
|
// Start receiving messages
|
|
isReceiving = true
|
|
receiveNextMessage()
|
|
|
|
// Send initial ping to verify connection
|
|
do {
|
|
try await sendPing()
|
|
Task { @MainActor in
|
|
self.delegate?.webSocketDidConnect(self)
|
|
}
|
|
} catch {
|
|
Task { @MainActor in
|
|
self.delegate?.webSocket(self, didFailWithError: error)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func send(_ message: WebSocketMessage) async throws {
|
|
guard let task = webSocketTask else {
|
|
throw WebSocketError.connectionFailed
|
|
}
|
|
|
|
switch message {
|
|
case .string(let text):
|
|
try await task.send(.string(text))
|
|
case .data(let data):
|
|
try await task.send(.data(data))
|
|
}
|
|
}
|
|
|
|
func sendPing() async throws {
|
|
guard let task = webSocketTask else {
|
|
throw WebSocketError.connectionFailed
|
|
}
|
|
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
task.sendPing { error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func disconnect(with code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
|
isReceiving = false
|
|
webSocketTask?.cancel(with: code, reason: reason)
|
|
Task { @MainActor in
|
|
self.delegate?.webSocketDidDisconnect(self, closeCode: code, reason: reason)
|
|
}
|
|
}
|
|
|
|
private func receiveNextMessage() {
|
|
guard isReceiving, let task = webSocketTask else { return }
|
|
|
|
task.receive { [weak self] result in
|
|
guard let self else { return }
|
|
|
|
switch result {
|
|
case .success(let message):
|
|
let wsMessage: WebSocketMessage
|
|
switch message {
|
|
case .string(let text):
|
|
wsMessage = .string(text)
|
|
case .data(let data):
|
|
wsMessage = .data(data)
|
|
@unknown default:
|
|
return
|
|
}
|
|
|
|
Task { @MainActor in
|
|
self.delegate?.webSocket(self, didReceiveMessage: wsMessage)
|
|
}
|
|
|
|
// Continue receiving
|
|
Task { @MainActor in
|
|
self.receiveNextMessage()
|
|
}
|
|
|
|
case .failure(let error):
|
|
Task { @MainActor in
|
|
self.isReceiving = false
|
|
self.delegate?.webSocket(self, didFailWithError: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension URLSessionWebSocket: URLSessionWebSocketDelegate {
|
|
nonisolated func urlSession(
|
|
_ session: URLSession,
|
|
webSocketTask: URLSessionWebSocketTask,
|
|
didOpenWithProtocol protocol: String?
|
|
) {
|
|
// Connection opened - already handled in connect()
|
|
}
|
|
|
|
nonisolated func urlSession(
|
|
_ session: URLSession,
|
|
webSocketTask: URLSessionWebSocketTask,
|
|
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
|
|
reason: Data?
|
|
) {
|
|
Task { @MainActor in
|
|
self.isReceiving = false
|
|
self.delegate?.webSocketDidDisconnect(self, closeCode: closeCode, reason: reason)
|
|
}
|
|
}
|
|
}
|