diff --git a/clients/macos/MagnumOpus/Services/SSEClient.swift b/clients/macos/MagnumOpus/Services/SSEClient.swift new file mode 100644 index 0000000..fc013d0 --- /dev/null +++ b/clients/macos/MagnumOpus/Services/SSEClient.swift @@ -0,0 +1,67 @@ +import Foundation + +struct ServerEvent { + let id: String? + let event: String? + let data: String +} + +@Observable +final class SSEClient { + private let url: URL + private var task: Task? + + var lastEvent: ServerEvent? + + init(url: URL) { + self.url = url + } + + func connect(onEvent: @escaping (ServerEvent) -> Void) { + task = Task { + do { + var request = URLRequest(url: url) + request.setValue("text/event-stream", forHTTPHeaderField: "Accept") + + let (bytes, _) = try await URLSession.shared.bytes(for: request) + + var currentEvent: String? + var currentData = "" + var currentId: String? + + for try await line in bytes.lines { + if line.isEmpty { + if !currentData.isEmpty { + let event = ServerEvent( + id: currentId, + event: currentEvent, + data: currentData.trimmingCharacters(in: .newlines) + ) + self.lastEvent = event + onEvent(event) + currentEvent = nil + currentData = "" + currentId = nil + } + } else if line.hasPrefix("data: ") { + currentData += String(line.dropFirst(6)) + "\n" + } else if line.hasPrefix("event: ") { + currentEvent = String(line.dropFirst(7)) + } else if line.hasPrefix("id: ") { + currentId = String(line.dropFirst(4)) + } + } + } catch { + if !Task.isCancelled { + try? await Task.sleep(for: .seconds(5)) + connect(onEvent: onEvent) + } + } + } + } + + func disconnect() { + task?.cancel() + task = nil + } +}