HeartbeatsBundle.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. /// A type that can be converted to a `HeartbeatsPayload`.
  16. protocol HeartbeatsPayloadConvertible {
  17. func makeHeartbeatsPayload() -> HeartbeatsPayload
  18. }
  19. /// A codable collection of heartbeats that has a fixed capacity and optimizations for storing heartbeats of
  20. /// multiple time periods.
  21. struct HeartbeatsBundle: Codable, HeartbeatsPayloadConvertible {
  22. /// The maximum number of heartbeats that can be stored in the buffer.
  23. let capacity: Int
  24. /// A cache used for keeping track of the last heartbeat date recorded for a given time period.
  25. ///
  26. /// The cache contains the last added date for each time period. The reason only the date is cached is
  27. /// because it's the only piece of information that should be used by clients to determine whether or not
  28. /// to append a new heartbeat.
  29. private(set) var lastAddedHeartbeatDates: [TimePeriod: Date]
  30. /// A ring buffer of heartbeats.
  31. private var buffer: RingBuffer<Heartbeat>
  32. /// A default cache provider that provides a dictionary of all time periods mapping to a default date.
  33. static var cacheProvider: () -> [TimePeriod: Date] {
  34. let timePeriodsAndDates = TimePeriod.allCases.map { ($0, Date.distantPast) }
  35. return { Dictionary(uniqueKeysWithValues: timePeriodsAndDates) }
  36. }
  37. /// Designated initializer.
  38. /// - Parameters:
  39. /// - capacity: The heartbeat capacity of the inititialized collection.
  40. /// - cache: A cache of time periods mapping to dates. Defaults to using static `cacheProvider`.
  41. init(capacity: Int,
  42. cache: [TimePeriod: Date] = cacheProvider()) {
  43. buffer = RingBuffer(capacity: capacity)
  44. self.capacity = capacity
  45. lastAddedHeartbeatDates = cache
  46. }
  47. /// Appends a heartbeat to this collection.
  48. /// - Parameter heartbeat: The heartbeat to append.
  49. mutating func append(_ heartbeat: Heartbeat) {
  50. guard capacity > 0 else {
  51. return // Do not append if capacity is non-positive.
  52. }
  53. // 1. Push the heartbeat to the back of the buffer.
  54. if let overwrittenHeartbeat = buffer.push(heartbeat) {
  55. // If a heartbeat was overwritten, update the cache to ensure it's date
  56. // is removed (if it was stored).
  57. lastAddedHeartbeatDates = lastAddedHeartbeatDates.mapValues { date in
  58. overwrittenHeartbeat.date == date ? .distantPast : date
  59. }
  60. }
  61. // 2. Update cache with the new heartbeat's date.
  62. heartbeat.timePeriods.forEach {
  63. lastAddedHeartbeatDates[$0] = heartbeat.date
  64. }
  65. }
  66. /// Removes the heartbeat associated with the given date.
  67. /// - Parameter date: The date of the heartbeat needing removal.
  68. /// - Returns: The heartbeat that was removed or `nil` if there was no heartbeat to remove.
  69. @discardableResult
  70. mutating func removeHeartbeat(from date: Date) -> Heartbeat? {
  71. var removedHeartbeat: Heartbeat?
  72. var poppedHeartbeats: [Heartbeat] = []
  73. while let poppedHeartbeat = buffer.pop() {
  74. if poppedHeartbeat.date == date {
  75. removedHeartbeat = poppedHeartbeat
  76. break
  77. }
  78. poppedHeartbeats.append(poppedHeartbeat)
  79. }
  80. poppedHeartbeats.reversed().forEach {
  81. buffer.push($0)
  82. }
  83. return removedHeartbeat
  84. }
  85. /// Makes and returns a `HeartbeatsPayload` from this heartbeats bundle.
  86. /// - Returns: A heartbeats payload.
  87. func makeHeartbeatsPayload() -> HeartbeatsPayload {
  88. let agentAndDates = buffer.map { heartbeat in
  89. (heartbeat.agent, [heartbeat.date])
  90. }
  91. let userAgentPayloads = [String: [Date]](agentAndDates, uniquingKeysWith: +)
  92. .map(HeartbeatsPayload.UserAgentPayload.init)
  93. .sorted { $0.agent < $1.agent } // Sort payloads by user agent.
  94. return HeartbeatsPayload(userAgentPayloads: userAgentPayloads)
  95. }
  96. }