import Foundation public enum WatchSyncKind: String, Codable, Sendable { case liveHeartRate case measurementUpsert case regulationSessionUpsert case feelingUpsert case tombstone case preferences case acknowledgement = "ack" } /// The payload is encoded JSON data. Keeping the transport wrapper non-generic lets the /// offline outbox contain measurements, sessions and tombstones in a single queue. public struct WatchSyncEnvelopeV1: Codable, Equatable, Sendable, Identifiable { public static let schemaVersion = 1 public var id: UUID { eventID } public let eventID: UUID public let deviceID: String public let sequence: UInt64 public let sentAt: Date public let kind: WatchSyncKind public let payload: Data public init( eventID: UUID = UUID(), deviceID: String, sequence: UInt64, sentAt: Date = Date(), kind: WatchSyncKind, payload: Data ) { self.eventID = eventID self.deviceID = deviceID self.sequence = sequence self.sentAt = sentAt self.kind = kind self.payload = payload } public init( eventID: UUID = UUID(), deviceID: String, sequence: UInt64, sentAt: Date = Date(), kind: WatchSyncKind, payload: Payload, encoder: JSONEncoder = WatchCoding.encoder ) throws { self.init( eventID: eventID, deviceID: deviceID, sequence: sequence, sentAt: sentAt, kind: kind, payload: try encoder.encode(payload) ) } public func decodePayload( _ type: Payload.Type, decoder: JSONDecoder = WatchCoding.decoder ) throws -> Payload { try decoder.decode(type, from: payload) } private enum CodingKeys: String, CodingKey { case schemaVersion, eventID = "eventId", deviceID = "deviceId", sequence, sentAt, kind, payload } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(Self.schemaVersion, forKey: .schemaVersion) try container.encode(eventID, forKey: .eventID) try container.encode(deviceID, forKey: .deviceID) try container.encode(sequence, forKey: .sequence) try container.encode(sentAt, forKey: .sentAt) try container.encode(kind, forKey: .kind) try container.encode(try WatchJSONValue(data: payload), forKey: .payload) } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) guard try container.decode(Int.self, forKey: .schemaVersion) == Self.schemaVersion else { throw DecodingError.dataCorruptedError(forKey: .schemaVersion, in: container, debugDescription: "Unsupported Watch envelope schema") } eventID = try container.decode(UUID.self, forKey: .eventID) deviceID = try container.decode(String.self, forKey: .deviceID) sequence = try container.decode(UInt64.self, forKey: .sequence) sentAt = try container.decode(Date.self, forKey: .sentAt) kind = try container.decode(WatchSyncKind.self, forKey: .kind) payload = try container.decode(WatchJSONValue.self, forKey: .payload).data() } } private enum WatchJSONValue: Codable, Equatable { case object([String: WatchJSONValue]) case array([WatchJSONValue]) case string(String) case number(Double) case bool(Bool) case null init(data: Data) throws { self = try WatchCoding.decoder.decode(Self.self, from: data) } func data() throws -> Data { try WatchCoding.encoder.encode(self) } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null } else if let value = try? container.decode(Bool.self) { self = .bool(value) } else if let value = try? container.decode(Double.self) { self = .number(value) } else if let value = try? container.decode(String.self) { self = .string(value) } else if let value = try? container.decode([WatchJSONValue].self) { self = .array(value) } else { self = .object(try container.decode([String: WatchJSONValue].self)) } } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case let .object(value): try container.encode(value) case let .array(value): try container.encode(value) case let .string(value): try container.encode(value) case let .number(value): try container.encode(value) case let .bool(value): try container.encode(value) case .null: try container.encodeNil() } } } public struct WatchAcknowledgementV1: Codable, Equatable, Sendable { public let eventIDs: [UUID] public let receivedAt: Date public init(eventIDs: [UUID], receivedAt: Date = Date()) { self.eventIDs = eventIDs self.receivedAt = receivedAt } } public struct WatchLiveHeartRateV1: Codable, Equatable, Sendable { public let bpm: Double public let sampledAt: Date public let measurementID: UUID? public init(bpm: Double, sampledAt: Date, measurementID: UUID? = nil) { self.bpm = bpm self.sampledAt = sampledAt self.measurementID = measurementID } } public enum WatchCoding { public static var encoder: JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.sortedKeys] return encoder } public static var decoder: JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder } }