import Foundation import RemoteCore private func relayNSNumberIsBoolean(_ value: NSNumber) -> Bool { String(cString: value.objCType) == "c" } private func relayNSNumberIsInteger(_ value: NSNumber) -> Bool { ["c", "s", "i", "l", "q", "C", "S", "I", "L", "Q"].contains( String(cString: value.objCType)) } /// Typed outgoing container seam. Workstream B owns WCSession dictionary parsing, /// strict incoming key/type validation, routing and exactly-once replies. public enum RelayContainerError: Error, Sendable, Equatable { case missingKeys case extraKeys case invalidSchemaType case unsupportedSchema(Int) case invalidKind case invalidPayload case unexpectedKind(RelayMessageKind) case malformedPayload } public struct RelayContainer: Sendable { public let schemaVersion = RelayWire.schemaVersion public let kind: RelayMessageKind public let payload: Data public init(kind: RelayMessageKind, value: Value) throws { self.kind = kind self.payload = try RelayJSON.encode(value) } /// The exact WCSession dictionary shape. Parsing is structural only; the /// caller must decode the payload for the expected request kind. public init(dictionary: [String: Any]) throws { let expected: Set = ["schemaVersion", "kind", "payload"] let actual = Set(dictionary.keys) guard actual == expected else { if actual.isSubset(of: expected) { throw RelayContainerError.missingKeys } throw RelayContainerError.extraKeys } guard let version = dictionary["schemaVersion"] as? NSNumber, CFGetTypeID(version) == CFNumberGetTypeID(), relayNSNumberIsInteger(version), version.intValue >= 0, !relayNSNumberIsBoolean(version) else { throw RelayContainerError.invalidSchemaType } let schema = version.intValue guard schema == RelayWire.schemaVersion else { throw RelayContainerError.unsupportedSchema(schema) } guard let rawKind = dictionary["kind"] as? String, let kind = RelayMessageKind(rawValue: rawKind) else { throw RelayContainerError.invalidKind } guard let payload = dictionary["payload"] as? Data else { throw RelayContainerError.invalidPayload } guard payload.count <= RelayWire.maximumPayloadBytes else { throw RelayContractError.payloadTooLarge } self.kind = kind self.payload = payload } public func dictionary() -> [String: Any] { ["schemaVersion": schemaVersion, "kind": kind.rawValue, "payload": payload] } public func decode(_ type: Value.Type, expectedKind: RelayMessageKind) throws -> Value { guard kind == expectedKind else { throw RelayContainerError.unexpectedKind(kind) } do { return try RelayJSON.decode(type, from: payload) } catch { throw RelayContainerError.malformedPayload } } } /// Only allowlisted, latest-state configuration belongs in application /// context. Commands, tokens and executable work are intentionally absent. public struct RelayApplicationContext: Sendable, Equatable { public let schemaVersion: Int public let selectedTelevisionID: UUID? public let displayName: String? public init(selectedTelevisionID: UUID?, displayName: String?) { self.schemaVersion = RelayWire.schemaVersion self.selectedTelevisionID = selectedTelevisionID self.displayName = displayName } public func dictionary() throws -> [String: Any] { var result: [String: Any] = ["schemaVersion": schemaVersion] if let selectedTelevisionID { result["selectedTelevisionID"] = selectedTelevisionID.uuidString } if let displayName { result["displayName"] = String(displayName.prefix(128)) } return result } public init(dictionary: [String: Any]) throws { let allowed: Set = ["schemaVersion", "selectedTelevisionID", "displayName"] guard Set(dictionary.keys).isSubset(of: allowed) else { throw RelayContainerError.extraKeys } guard let version = dictionary["schemaVersion"] as? NSNumber, relayNSNumberIsInteger(version), !relayNSNumberIsBoolean(version), version.intValue == RelayWire.schemaVersion else { throw RelayContainerError.invalidSchemaType } if let raw = dictionary["selectedTelevisionID"] { guard let string = raw as? String, UUID(uuidString: string) != nil else { throw RelayContainerError.invalidPayload } self.selectedTelevisionID = UUID(uuidString: string) } else { self.selectedTelevisionID = nil } if let raw = dictionary["displayName"] { guard let string = raw as? String, string.count <= 128 else { throw RelayContainerError.invalidPayload } self.displayName = string } else { self.displayName = nil } self.schemaVersion = version.intValue } } public enum RelayActivationState: Sendable, Equatable { case notActivated, activating, activated, inactive, deactivated } /// A small injectable facade around WCSession. It deliberately exposes only /// immediate messages and latest-state context, never transferUserInfo. @MainActor public protocol RelaySessionSending: AnyObject { var activationState: RelayActivationState { get } var isReachable: Bool { get } func activate() func sendMessage(_ message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void, errorHandler: @escaping (Error) -> Void) func updateApplicationContext(_ context: [String: Any]) throws } public final class RelayReplyOnce: @unchecked Sendable { private let lock = NSLock() private var completed = false private let handler: ([String: Any]) -> Void public init(_ handler: @escaping ([String: Any]) -> Void) { self.handler = handler } public func reply(_ value: [String: Any]) { lock.lock() guard !completed else { lock.unlock(); return } completed = true lock.unlock() handler(value) } } /// WCSession delivers Foundation dictionaries and non-Sendable callbacks from /// a delegate context. This explicit boundary lets the native delegate hop to /// the main actor without pretending the dictionary itself is Sendable. public final class RelayInboundMessage: @unchecked Sendable { public let message: [String: Any] public let reply: RelayReplyOnce public init(message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { self.message = message self.reply = RelayReplyOnce(replyHandler) } } public func relayErrorReply(_ failure: RelayFailure) -> [String: Any] { (try? RelayContainer(kind: .error, value: failure).dictionary()) ?? [ "schemaVersion": RelayWire.schemaVersion, "kind": RelayMessageKind.error.rawValue, "payload": Data() ] }