import Foundation
import IrdP2PCrypto
/// Frames a client sends on `/ws`. Every frame is a JSON text message with a `t` field.
public enum ClientFrame: Sendable, Equatable {
case join(room: RoomId, peer: PeerId)
case signal(to: PeerId, payload: JSONValue)
case leave
case ping
/// The wire text, keys in the documented order.
public var encoded: String { json.serialized }
public var json: JSONValue {
switch self {
case .join(let room, let peer):
return .object(JSONObject([("t", .string("join")), ("room", .string(room.string)), ("peer", .string(peer.hex))]))
case .signal(let to, let payload):
return .object(JSONObject([("t", .string("signal")), ("to", .string(to.hex)), ("payload", payload)]))
case .leave: return .object(["t": .string("leave")])
case .ping: return .object(["t": .string("ping")])
}
}
}
/// Frames the relay sends. Unknown types decode to `.unknown` and unknown keys are
/// ignored: additive protocol changes ship without a version bump.
public enum ServerFrame: Sendable, Equatable {
/// Reply to `join`: who is already in the room (never includes yourself).
case peers([PeerId])
case peerJoined(PeerId)
case peerLeft(PeerId)
/// A forwarded payload; `from` is stamped by the relay and cannot be spoofed.
case signal(from: PeerId, payload: JSONValue)
case pong
case error(RelayErrorCode)
case unknown(type: String)
public struct DecodeError: Error, Sendable, Equatable { public let reason: String }
public static func decode(_ text: String) throws -> ServerFrame {
let v: JSONValue
do { v = try JSONValue.parse(text) } catch { throw DecodeError(reason: "not JSON: \(error)") }
return try decode(v)
}
public static func decode(_ v: JSONValue) throws -> ServerFrame {
guard case .object(let o) = v, let t = o["t"]?.stringValue else { throw DecodeError(reason: "missing t") }
switch t {
case "peers":
let list = o["peers"]?.arrayValue ?? []
return .peers(try list.map { try peer($0) })
case "peer-joined": return .peerJoined(try peer(o["peer"]))
case "peer-left": return .peerLeft(try peer(o["peer"]))
case "signal":
guard let payload = o["payload"] else { throw DecodeError(reason: "signal without payload") }
return .signal(from: try peer(o["from"]), payload: payload)
case "pong": return .pong
case "error": return .error(RelayErrorCode(o["code"]?.stringValue ?? ""))
default: return .unknown(type: t)
}
}
private static func peer(_ v: JSONValue?) throws -> PeerId {
guard let s = v?.stringValue, let p = try? PeerId(s) else { throw DecodeError(reason: "bad peer id") }
return p
}
}