import Foundation struct HistoryItem { /// The sent/received date (local time, not including compensation offset) let requestDate: Date let request: Message.Content let usedLocalConnection: Bool let response: ClientState? let responseMessage: Message.Content? let responseDate: Date? init(sent message: Message.Content, date: Date, local: Bool) { self.requestDate = date self.request = message self.responseMessage = nil self.response = nil self.responseDate = nil self.usedLocalConnection = local } func didReceive(response: ClientState, date: Date?, message: Message.Content?) -> HistoryItem { .init(sent: self, response: response, date: date, message: message) } func invalidated() -> HistoryItem { didReceive(response: .responseRejected(.invalidAuthentication), date: responseDate, message: responseMessage) } func notAuthenticated() -> HistoryItem { didReceive(response: .responseRejected(.missingKey), date: responseDate, message: responseMessage) } private init(sent: HistoryItem, response: ClientState, date: Date?, message: Message.Content?) { self.requestDate = sent.requestDate self.request = sent.request self.responseDate = date self.responseMessage = message self.response = response self.usedLocalConnection = sent.usedLocalConnection } // MARK: Statistics var roundTripTime: TimeInterval? { responseDate?.timeIntervalSince(requestDate) } var deviceTime: Date? { guard let timestamp = responseMessage?.time else { return nil } return Date(timestamp: timestamp) } var requestLatency: TimeInterval? { deviceTime?.timeIntervalSince(requestDate) } var responseLatency: TimeInterval? { guard let deviceTime = deviceTime else { return nil } return responseDate?.timeIntervalSince(deviceTime) } var clockOffset: Int? { guard let interval = roundTripTime, let deviceTime = deviceTime else { return nil } let estimatedArrival = requestDate.advanced(by: interval / 2) return Int(deviceTime.timeIntervalSince(estimatedArrival)) } } extension HistoryItem: Codable { enum CodingKeys: Int, CodingKey { case requestDate = 1 case request = 2 case usedLocalConnection = 3 case response = 4 case responseMessage = 5 case responseDate = 6 } } extension ClientState: Codable { init(from decoder: Decoder) throws { let code = try decoder.singleValueContainer().decode(UInt8.self) self.init(code: code) } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(code) } } extension HistoryItem: Identifiable { var id: UInt32 { requestDate.timestamp } } extension HistoryItem: Comparable { static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool { lhs.requestDate < rhs.requestDate } } extension HistoryItem { static var mock: HistoryItem { let content = Message.Content(time: Date.now.timestamp, id: 123, device: 0) let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124, device: 0) return .init(sent: content, date: .now, local: false) .didReceive(response: .openSesame, date: .now + 2, message: content2) } }