CgmEventStore.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. //
  2. // CgmEventStore.swift
  3. // LoopKit
  4. //
  5. // Created by Pete Schwamb on 9/9/23.
  6. // Copyright © 2023 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import CoreData
  10. import HealthKit
  11. import os.log
  12. public protocol CgmEventStoreDelegate: AnyObject {
  13. /**
  14. Informs the delegate that the cgm event store has updated event data.
  15. - Parameter cgmEventStore: The cgm event store that has updated event data.
  16. */
  17. func cgmEventStoreHasUpdatedData(_ cgmEventStore: CgmEventStore)
  18. }
  19. /**
  20. Manages storage and retrieval of cgm events
  21. */
  22. public final class CgmEventStore {
  23. public weak var delegate: CgmEventStoreDelegate?
  24. /// The interval of cgm event data to keep in cache
  25. public let cacheLength: TimeInterval
  26. private let log = OSLog(category: "CgmEventStore")
  27. private let cacheStore: PersistenceController
  28. private let queue = DispatchQueue(label: "com.loopkit.CgmEventStore.queue", qos: .utility)
  29. // MARK: - ReadyState
  30. private enum ReadyState {
  31. case waiting
  32. case ready
  33. case error(Error)
  34. }
  35. public typealias ReadyCallback = (_ error: Error?) -> Void
  36. private var readyCallbacks: [ReadyCallback] = []
  37. private var readyState: ReadyState = .waiting
  38. public func onReady(_ callback: @escaping ReadyCallback) {
  39. queue.async {
  40. switch self.readyState {
  41. case .waiting:
  42. self.readyCallbacks.append(callback)
  43. case .ready:
  44. callback(nil)
  45. case .error(let error):
  46. callback(error)
  47. }
  48. }
  49. }
  50. /// The maximum length of time to keep data around.
  51. public var cacheStartDate: Date {
  52. return Date().addingTimeInterval(-cacheLength)
  53. }
  54. public init(
  55. cacheStore: PersistenceController,
  56. cacheLength: TimeInterval = 60 /* minutes */ * 60 /* seconds */
  57. ) {
  58. self.cacheStore = cacheStore
  59. self.cacheLength = cacheLength
  60. cacheStore.onReady { (error) in
  61. guard error == nil else {
  62. self.queue.async {
  63. self.readyState = .error(error!)
  64. for callback in self.readyCallbacks {
  65. callback(error)
  66. }
  67. self.readyCallbacks = []
  68. }
  69. return
  70. }
  71. cacheStore.fetchAnchor(key: GlucoseStore.healthKitQueryAnchorMetadataKey) { (anchor) in
  72. self.queue.async {
  73. self.readyState = .ready
  74. for callback in self.readyCallbacks {
  75. callback(error)
  76. }
  77. self.readyCallbacks = []
  78. }
  79. }
  80. }
  81. }
  82. }
  83. // MARK: - Fetching
  84. extension CgmEventStore {
  85. public struct QueryAnchor: Equatable, RawRepresentable {
  86. public typealias RawValue = [String: Any]
  87. internal var modificationCounter: Int64
  88. public init() {
  89. self.modificationCounter = 0
  90. }
  91. public init?(rawValue: RawValue) {
  92. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  93. return nil
  94. }
  95. self.modificationCounter = modificationCounter
  96. }
  97. public var rawValue: RawValue {
  98. var rawValue: RawValue = [:]
  99. rawValue["modificationCounter"] = modificationCounter
  100. return rawValue
  101. }
  102. }
  103. /**
  104. Adds and persists a new cgm event
  105. - parameter unitVolume: The reservoir volume, in units
  106. - parameter date: The date of the volume reading
  107. - parameter completion: A closure called after the value was saved. This closure takes three arguments:
  108. - value: The new reservoir value, if it was saved
  109. - previousValue: The last new reservoir value
  110. - areStoredValuesContinous: Whether the current recent state of the stored reservoir data is considered continuous and reliable for deriving insulin effects after addition of this new value.
  111. - error: An error object explaining why the value could not be saved
  112. */
  113. public func add(events: [PersistedCgmEvent]) async throws {
  114. try await cacheStore.managedObjectContext.perform {
  115. for event in events {
  116. let cgmEvent = CgmEvent(context: self.cacheStore.managedObjectContext)
  117. cgmEvent.date = event.date
  118. cgmEvent.type = event.type
  119. cgmEvent.deviceIdentifier = event.deviceIdentifier
  120. cgmEvent.expectedLifetime = event.expectedLifetime
  121. cgmEvent.warmupPeriod = event.warmupPeriod
  122. cgmEvent.failureMessage = event.failureMessage
  123. cgmEvent.storedAt = Date()
  124. }
  125. if let error = self.cacheStore.save() {
  126. self.log.error("Error saving CGM event: %{public}@", error.localizedDescription)
  127. throw error
  128. }
  129. try self.purgeOldCgmEvents()
  130. self.delegate?.cgmEventStoreHasUpdatedData(self)
  131. }
  132. }
  133. public enum CgmEventQueryResult {
  134. case success(QueryAnchor, [PersistedCgmEvent])
  135. case failure(Error)
  136. }
  137. public func executeCgmEventQuery(fromQueryAnchor queryAnchor: QueryAnchor?, completion: @escaping (CgmEventQueryResult) -> Void) {
  138. var queryAnchor = queryAnchor ?? QueryAnchor()
  139. var queryResult = [PersistedCgmEvent]()
  140. var queryError: Error?
  141. cacheStore.managedObjectContext.performAndWait {
  142. let storedRequest: NSFetchRequest<CgmEvent> = CgmEvent.fetchRequest()
  143. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  144. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  145. do {
  146. let stored = try self.cacheStore.managedObjectContext.fetch(storedRequest)
  147. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  148. queryAnchor.modificationCounter = modificationCounter
  149. }
  150. queryResult.append(contentsOf: stored.compactMap { $0.persistedCgmEvent })
  151. } catch let error {
  152. queryError = error
  153. }
  154. }
  155. if let queryError = queryError {
  156. completion(.failure(queryError))
  157. return
  158. }
  159. completion(.success(queryAnchor, queryResult))
  160. }
  161. private func purgeOldCgmEvents() throws {
  162. let predicate = NSPredicate(format: "storedAt < %@", cacheStartDate as NSDate)
  163. let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: CgmEvent.entity().name!)
  164. fetchRequest.predicate = predicate
  165. let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
  166. deleteRequest.resultType = .resultTypeObjectIDs
  167. do {
  168. if let result = try cacheStore.managedObjectContext.execute(deleteRequest) as? NSBatchDeleteResult,
  169. let objectIDs = result.result as? [NSManagedObjectID],
  170. objectIDs.count > 0
  171. {
  172. let changes = [NSDeletedObjectsKey: objectIDs]
  173. NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [cacheStore.managedObjectContext])
  174. }
  175. } catch let error as NSError {
  176. throw PersistenceController.PersistenceControllerError.coreDataError(error)
  177. }
  178. }
  179. }