InsulinDeliveryStore.swift 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. //
  2. // InsulinDeliveryStore.swift
  3. // InsulinKit
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import HealthKit
  8. import CoreData
  9. import os.log
  10. public protocol InsulinDeliveryStoreDelegate: AnyObject {
  11. /**
  12. Informs the delegate that the insulin delivery store has updated dose data.
  13. - Parameter insulinDeliveryStore: The insulin delivery store that has updated dose data.
  14. */
  15. func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore)
  16. }
  17. /// Manages insulin dose data in Core Data and optionally reads insulin dose data from HealthKit.
  18. ///
  19. /// Scheduled doses (e.g. a bolus or temporary basal) shouldn't be written to this store until they've
  20. /// been delivered into the patient, which means its common for this store data to slightly lag
  21. /// behind the dose data used for algorithmic calculation.
  22. ///
  23. /// This store data isn't a substitute for an insulin pump's diagnostic event history, but doses fetched
  24. /// from this store can reduce the amount of repeated communication with an insulin pump.
  25. public class InsulinDeliveryStore: HealthKitSampleStore {
  26. /// Notification posted when dose entries were changed, either via direct add or from HealthKit
  27. public static let doseEntriesDidChange = NSNotification.Name(rawValue: "com.loopkit.InsulinDeliveryStore.doseEntriesDidChange")
  28. private let insulinQuantityType = HKQuantityType.quantityType(forIdentifier: .insulinDelivery)!
  29. private let queue = DispatchQueue(label: "com.loopkit.InsulinDeliveryStore.queue", qos: .utility)
  30. private let log = OSLog(category: "InsulinDeliveryStore")
  31. /// The most-recent end date for an immutable basal dose entry written by LoopKit
  32. /// Should only be accessed on queue
  33. private var lastImmutableBasalEndDate: Date? {
  34. didSet {
  35. test_lastImmutableBasalEndDateDidSet?()
  36. }
  37. }
  38. internal var test_lastImmutableBasalEndDateDidSet: (() -> Void)?
  39. public weak var delegate: InsulinDeliveryStoreDelegate?
  40. /// The interval of insulin delivery data to keep in cache
  41. public let cacheLength: TimeInterval
  42. private let storeSamplesToHealthKit: Bool
  43. private let cacheStore: PersistenceController
  44. private let provenanceIdentifier: String
  45. static let healthKitQueryAnchorMetadataKey = "com.loopkit.InsulinDeliveryStore.hkQueryAnchor"
  46. public init(
  47. healthStore: HKHealthStore,
  48. observeHealthKitSamplesFromOtherApps: Bool = true,
  49. storeSamplesToHealthKit: Bool = true,
  50. cacheStore: PersistenceController,
  51. observationEnabled: Bool = true,
  52. cacheLength: TimeInterval = 24 /* hours */ * 60 /* minutes */ * 60 /* seconds */,
  53. provenanceIdentifier: String,
  54. test_currentDate: Date? = nil
  55. ) {
  56. self.storeSamplesToHealthKit = storeSamplesToHealthKit
  57. self.cacheStore = cacheStore
  58. self.cacheLength = cacheLength
  59. self.provenanceIdentifier = provenanceIdentifier
  60. // Only observe HK driven changes from last 24 hours
  61. let observationStartOffset = min(cacheLength, .hours(24))
  62. super.init(
  63. healthStore: healthStore,
  64. observeHealthKitSamplesFromCurrentApp: true,
  65. observeHealthKitSamplesFromOtherApps: observeHealthKitSamplesFromOtherApps,
  66. type: insulinQuantityType,
  67. observationStart: (test_currentDate ?? Date()).addingTimeInterval(-observationStartOffset),
  68. observationEnabled: observationEnabled,
  69. test_currentDate: test_currentDate
  70. )
  71. let semaphore = DispatchSemaphore(value: 0)
  72. cacheStore.onReady { (error) in
  73. guard error == nil else {
  74. semaphore.signal()
  75. return
  76. }
  77. cacheStore.fetchAnchor(key: InsulinDeliveryStore.healthKitQueryAnchorMetadataKey) { (anchor) in
  78. self.queue.async {
  79. self.queryAnchor = anchor
  80. if !self.authorizationRequired {
  81. self.createQuery()
  82. }
  83. self.updateLastImmutableBasalEndDate()
  84. semaphore.signal()
  85. }
  86. }
  87. }
  88. semaphore.wait()
  89. }
  90. // MARK: - HealthKitSampleStore
  91. override func queryAnchorDidChange() {
  92. cacheStore.storeAnchor(queryAnchor, key: InsulinDeliveryStore.healthKitQueryAnchorMetadataKey)
  93. }
  94. override func processResults(from query: HKAnchoredObjectQuery, added: [HKSample], deleted: [HKDeletedObject], anchor: HKQueryAnchor, completion: @escaping (Bool) -> Void) {
  95. queue.async {
  96. guard anchor != self.queryAnchor else {
  97. self.log.default("Skipping processing results from anchored object query, as anchor was already processed")
  98. completion(true)
  99. return
  100. }
  101. var changed = false
  102. var error: Error?
  103. self.cacheStore.managedObjectContext.performAndWait {
  104. do {
  105. // Add new samples
  106. if let samples = added as? [HKQuantitySample] {
  107. for sample in samples {
  108. if try self.addDoseEntry(for: sample) {
  109. self.log.debug("Saved sample %@ into cache from HKAnchoredObjectQuery", sample.uuid.uuidString)
  110. changed = true
  111. } else {
  112. self.log.default("Sample %@ from HKAnchoredObjectQuery already present in cache", sample.uuid.uuidString)
  113. }
  114. }
  115. }
  116. // Delete deleted samples
  117. let count = try self.deleteDoseEntries(withUUIDs: deleted.map { $0.uuid })
  118. if count > 0 {
  119. self.log.debug("Deleted %d samples from cache from HKAnchoredObjectQuery", count)
  120. changed = true
  121. }
  122. guard changed else {
  123. return
  124. }
  125. error = self.cacheStore.save()
  126. } catch let coreDataError {
  127. error = coreDataError
  128. }
  129. }
  130. guard error == nil else {
  131. completion(false)
  132. return
  133. }
  134. guard changed else {
  135. completion(true)
  136. return
  137. }
  138. self.handleUpdatedDoseData()
  139. self.delegate?.insulinDeliveryStoreHasUpdatedDoseData(self)
  140. completion(true)
  141. }
  142. }
  143. }
  144. // MARK: - Fetching
  145. extension InsulinDeliveryStore {
  146. /// Retrieves dose entries within the specified date range.
  147. ///
  148. /// - Parameters:
  149. /// - start: The earliest date of dose entries to retrieve, if provided.
  150. /// - end: The latest date of dose entries to retrieve, if provided.
  151. /// - includeMutable: Whether to include mutable dose entries or not. Defaults to false.
  152. /// - completion: A closure called once the dose entries have been retrieved.
  153. /// - result: An array of dose entries, in chronological order by startDate, or error.
  154. public func getDoseEntries(start: Date? = nil, end: Date? = nil, includeMutable: Bool = false, completion: @escaping (_ result: Result<[DoseEntry], Error>) -> Void) {
  155. queue.async {
  156. completion(self.getDoseEntries(start: start, end: end, includeMutable: includeMutable))
  157. }
  158. }
  159. private func getDoseEntries(start: Date? = nil, end: Date? = nil, includeMutable: Bool = false) -> Result<[DoseEntry], Error> {
  160. dispatchPrecondition(condition: .onQueue(queue))
  161. var entries: [DoseEntry] = []
  162. var error: Error?
  163. cacheStore.managedObjectContext.performAndWait {
  164. do {
  165. entries = try self.getCachedInsulinDeliveryObjects(start: start, end: end, includeMutable: includeMutable).map { $0.dose }
  166. } catch let coreDataError {
  167. error = coreDataError
  168. }
  169. }
  170. if let error = error {
  171. self.log.error("Error getting CachedInsulinDeliveryObjects: %{public}@", String(describing: error))
  172. return .failure(error)
  173. }
  174. return .success(entries)
  175. }
  176. private func getCachedInsulinDeliveryObjects(start: Date? = nil, end: Date? = nil, includeMutable: Bool = false) throws -> [CachedInsulinDeliveryObject] {
  177. dispatchPrecondition(condition: .onQueue(queue))
  178. // Match all doses whose start OR end dates fall in the start and end date range, if specified. Therefore, we ensure the
  179. // dose end date is AFTER the start date, if specified, and the dose start date is BEFORE the end date, if specified.
  180. var predicates = [NSPredicate(format: "deletedAt == NIL")]
  181. if let start = start {
  182. predicates.append(NSPredicate(format: "endDate >= %@", start as NSDate))
  183. }
  184. if let end = end {
  185. predicates.append(NSPredicate(format: "startDate <= %@", end as NSDate)) // Note: Using <= rather than < to match previous behavior
  186. }
  187. if !includeMutable {
  188. predicates.append(NSPredicate(format: "isMutable == NO"))
  189. }
  190. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  191. request.predicate = (predicates.count > 1) ? NSCompoundPredicate(andPredicateWithSubpredicates: predicates) : predicates.first
  192. request.sortDescriptors = [NSSortDescriptor(key: "startDate", ascending: true)]
  193. return try self.cacheStore.managedObjectContext.fetch(request)
  194. }
  195. /// Fetches manually entered doses.
  196. ///
  197. /// - Parameters:
  198. /// - startDate: The earliest dose startDate to include.
  199. /// - chronological: Whether to return the objects in chronological or reverse-chronological order.
  200. /// - limit: The maximum number of manually entered dose entries to return.
  201. /// - Returns: An array of manually entered dose dose entries in the specified order by date.
  202. public func getManuallyEnteredDoses(since startDate: Date, chronological: Bool = true, limit: Int? = nil, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) {
  203. queue.async {
  204. var doses: [DoseEntry] = []
  205. var error: DoseStore.DoseStoreError?
  206. self.cacheStore.managedObjectContext.performAndWait {
  207. let predicates = [NSPredicate(format: "deletedAt == NIL"),
  208. NSPredicate(format: "startDate >= %@", startDate as NSDate),
  209. NSPredicate(format: "manuallyEntered == YES")]
  210. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  211. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
  212. request.sortDescriptors = [NSSortDescriptor(key: "startDate", ascending: chronological)]
  213. if let limit = limit {
  214. request.fetchLimit = limit
  215. }
  216. do {
  217. doses = try self.cacheStore.managedObjectContext.fetch(request).compactMap{ $0.dose }
  218. } catch let fetchError as NSError {
  219. error = .fetchError(description: fetchError.localizedDescription, recoverySuggestion: fetchError.localizedRecoverySuggestion)
  220. } catch {
  221. assertionFailure()
  222. }
  223. }
  224. if let error = error {
  225. completion(.failure(error))
  226. }
  227. completion(.success(doses))
  228. }
  229. }
  230. /// Returns the end date of the most recent basal dose entry.
  231. ///
  232. /// - Parameters:
  233. /// - completion: A closure called when the date has been retrieved with date.
  234. /// - result: The date, or error.
  235. func getLastImmutableBasalEndDate(_ completion: @escaping (_ result: Result<Date, Error>) -> Void) {
  236. queue.async {
  237. switch self.lastImmutableBasalEndDate {
  238. case .some(let date):
  239. completion(.success(date))
  240. case .none:
  241. // TODO: send a proper error
  242. completion(.failure(DoseStore.DoseStoreError.configurationError))
  243. }
  244. }
  245. }
  246. private func updateLastImmutableBasalEndDate() {
  247. dispatchPrecondition(condition: .onQueue(queue))
  248. var endDate: Date?
  249. cacheStore.managedObjectContext.performAndWait {
  250. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  251. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "deletedAt == NIL"),
  252. NSPredicate(format: "reason == %d", HKInsulinDeliveryReason.basal.rawValue),
  253. NSPredicate(format: "hasLoopKitOrigin == YES"),
  254. NSPredicate(format: "isMutable == NO")])
  255. request.sortDescriptors = [NSSortDescriptor(key: "endDate", ascending: false)]
  256. request.fetchLimit = 1
  257. do {
  258. let objects = try self.cacheStore.managedObjectContext.fetch(request)
  259. endDate = objects.first?.endDate
  260. } catch let error {
  261. self.log.error("Unable to fetch latest insulin delivery objects: %@", String(describing: error))
  262. }
  263. }
  264. self.lastImmutableBasalEndDate = endDate ?? .distantPast
  265. }
  266. }
  267. // MARK: - Modification
  268. extension InsulinDeliveryStore {
  269. /// Add dose entries to store.
  270. ///
  271. /// - Parameters:
  272. /// - entries: The new dose entries to add to the store.
  273. /// - device: The optional device used for the new dose entries.
  274. /// - syncVersion: The sync version used for the new dose entries.
  275. /// - resolveMutable: Whether to update or delete any pre-existing mutable dose entries based upon any matching incoming mutable dose entries.
  276. /// - completion: A closure called once the dose entries have been stored.
  277. /// - result: Success or error.
  278. func addDoseEntries(_ entries: [DoseEntry], from device: HKDevice?, syncVersion: Int, resolveMutable: Bool = false, completion: @escaping (_ result: Result<Void, Error>) -> Void) {
  279. guard !entries.isEmpty else {
  280. completion(.success(()))
  281. return
  282. }
  283. queue.async {
  284. var changed = false
  285. var error: Error?
  286. self.cacheStore.managedObjectContext.performAndWait {
  287. do {
  288. let now = self.currentDate()
  289. var mutableObjects: [CachedInsulinDeliveryObject] = []
  290. // If we are resolving mutable objects, then fetch all non-deleted mutable objects and initially mark as deleted
  291. // If an incoming entry matches via syncIdentifier, then update and mark as NOT deleted
  292. if resolveMutable {
  293. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  294. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "deletedAt == NIL"),
  295. NSPredicate(format: "isMutable == YES")])
  296. mutableObjects = try self.cacheStore.managedObjectContext.fetch(request)
  297. mutableObjects.forEach { $0.deletedAt = now }
  298. }
  299. let resolvedSampleObjects: [(HKQuantitySample, CachedInsulinDeliveryObject)] = entries.compactMap { entry in
  300. guard entry.syncIdentifier != nil else {
  301. self.log.error("Ignored adding dose entry without sync identifier: %{public}@", String(reflecting: entry))
  302. return nil
  303. }
  304. guard let quantitySample = HKQuantitySample(type: self.insulinQuantityType,
  305. unit: HKUnit.internationalUnit(),
  306. dose: entry,
  307. device: device,
  308. provenanceIdentifier: self.provenanceIdentifier,
  309. syncVersion: syncVersion)
  310. else {
  311. self.log.error("Failure to create HKQuantitySample from DoseEntry: %{public}@", String(describing: entry))
  312. return nil
  313. }
  314. // If we have a mutable object that matches this sync identifier, then update, it will mark as NOT deleted
  315. if let object = mutableObjects.first(where: { $0.provenanceIdentifier == self.provenanceIdentifier && $0.syncIdentifier == entry.syncIdentifier }) {
  316. self.log.debug("Update: %{public}@", String(describing: entry))
  317. object.update(from: entry)
  318. return (quantitySample, object)
  319. // Otherwise, add new object
  320. } else {
  321. let object = CachedInsulinDeliveryObject(context: self.cacheStore.managedObjectContext)
  322. object.create(from: entry, by: self.provenanceIdentifier, at: now)
  323. self.log.debug("Add: %{public}@", String(describing: entry))
  324. return (quantitySample, object)
  325. }
  326. }
  327. for dose in mutableObjects {
  328. if dose.deletedAt != nil {
  329. self.log.debug("Delete: %{public}@", String(describing: dose))
  330. }
  331. }
  332. changed = !mutableObjects.isEmpty || !resolvedSampleObjects.isEmpty
  333. guard changed else {
  334. return
  335. }
  336. error = self.cacheStore.save()
  337. if error != nil {
  338. return
  339. }
  340. // Only save immutable objects to HealthKit
  341. self.saveEntriesToHealthKit(resolvedSampleObjects.filter { !$0.1.isMutable && !$0.1.isFault })
  342. } catch let coreDataError {
  343. error = coreDataError
  344. }
  345. }
  346. if let error = error {
  347. completion(.failure(error))
  348. return
  349. }
  350. guard changed else {
  351. completion(.success(()))
  352. return
  353. }
  354. self.handleUpdatedDoseData()
  355. self.delegate?.insulinDeliveryStoreHasUpdatedDoseData(self)
  356. completion(.success(()))
  357. }
  358. }
  359. private func saveEntriesToHealthKit(_ sampleObjects: [(HKQuantitySample, CachedInsulinDeliveryObject)]) {
  360. dispatchPrecondition(condition: .onQueue(queue))
  361. guard storeSamplesToHealthKit, !sampleObjects.isEmpty else {
  362. return
  363. }
  364. var error: Error?
  365. // Save objects to HealthKit, log any errors, but do not fail
  366. let dispatchGroup = DispatchGroup()
  367. dispatchGroup.enter()
  368. self.healthStore.save(sampleObjects.map { (sample, _) in sample }) { (_, healthKitError) in
  369. error = healthKitError
  370. dispatchGroup.leave()
  371. }
  372. dispatchGroup.wait()
  373. if let error = error {
  374. self.log.error("Error saving HealthKit objects: %@", String(describing: error))
  375. return
  376. }
  377. // Update Core Data with the changes, log any errors, but do not fail
  378. sampleObjects.forEach { (sample, object) in object.uuid = sample.uuid }
  379. if let error = self.cacheStore.save() {
  380. self.log.error("Error updating CachedInsulinDeliveryObjects after saving HealthKit objects: %@", String(describing: error))
  381. sampleObjects.forEach { (_, object) in object.uuid = nil }
  382. }
  383. }
  384. private func addDoseEntry(for sample: HKQuantitySample) throws -> Bool {
  385. dispatchPrecondition(condition: .onQueue(queue))
  386. // Is entire sample before earliest cache date?
  387. guard sample.endDate >= earliestCacheDate else {
  388. return false
  389. }
  390. // Are there any objects matching the UUID?
  391. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  392. request.predicate = NSPredicate(format: "uuid == %@", sample.uuid as NSUUID)
  393. request.fetchLimit = 1
  394. let count = try cacheStore.managedObjectContext.count(for: request)
  395. guard count == 0 else {
  396. return false
  397. }
  398. // Add an object for this UUID
  399. let object = CachedInsulinDeliveryObject(context: cacheStore.managedObjectContext)
  400. object.create(fromExisting: sample, on: self.currentDate())
  401. return true
  402. }
  403. func deleteDose(bySyncIdentifier syncIdentifier: String, _ completion: @escaping (String?) -> Void) {
  404. queue.async {
  405. var errorString: String? = nil
  406. self.cacheStore.managedObjectContext.performAndWait {
  407. do {
  408. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  409. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "deletedAt == NIL"),
  410. NSPredicate(format: "syncIdentifier == %@", syncIdentifier)])
  411. request.fetchBatchSize = 100
  412. let objects = try self.cacheStore.managedObjectContext.fetch(request)
  413. if !objects.isEmpty {
  414. let deletedAt = self.currentDate()
  415. for object in objects {
  416. object.deletedAt = deletedAt
  417. }
  418. self.cacheStore.save()
  419. }
  420. let healthKitPredicate = HKQuery.predicateForObjects(withMetadataKey: HKMetadataKeySyncIdentifier, allowedValues: [syncIdentifier])
  421. self.healthStore.deleteObjects(of: self.insulinQuantityType, predicate: healthKitPredicate)
  422. { success, deletedObjectCount, error in
  423. if let error = error {
  424. self.log.error("Unable to delete dose from Health: %@", error.localizedDescription)
  425. }
  426. }
  427. } catch let error {
  428. errorString = "Error deleting CachedInsulinDeliveryObject: " + error.localizedDescription
  429. return
  430. }
  431. }
  432. self.handleUpdatedDoseData()
  433. self.delegate?.insulinDeliveryStoreHasUpdatedDoseData(self)
  434. completion(errorString)
  435. }
  436. }
  437. func deleteDose(with uuidToDelete: UUID, _ completion: @escaping (String?) -> Void) {
  438. queue.async {
  439. var errorString: String? = nil
  440. self.cacheStore.managedObjectContext.performAndWait {
  441. do {
  442. let count = try self.deleteDoseEntries(withUUIDs: [uuidToDelete])
  443. guard count > 0 else {
  444. errorString = "Cannot find CachedInsulinDeliveryObject to delete"
  445. return
  446. }
  447. self.cacheStore.save()
  448. } catch let error {
  449. errorString = "Error deleting CachedInsulinDeliveryObject: " + error.localizedDescription
  450. return
  451. }
  452. }
  453. self.handleUpdatedDoseData()
  454. self.delegate?.insulinDeliveryStoreHasUpdatedDoseData(self)
  455. completion(errorString)
  456. }
  457. }
  458. private func deleteDoseEntries(withUUIDs uuids: [UUID], batchSize: Int = 500) throws -> Int {
  459. dispatchPrecondition(condition: .onQueue(queue))
  460. let deletedAt = self.currentDate()
  461. var count = 0
  462. for batch in uuids.chunked(into: batchSize) {
  463. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  464. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "deletedAt == NIL"),
  465. NSPredicate(format: "uuid IN %@", batch.map { $0 as NSUUID })])
  466. let objects = try self.cacheStore.managedObjectContext.fetch(request)
  467. for object in objects {
  468. object.deletedAt = deletedAt
  469. }
  470. count += objects.count
  471. }
  472. return count
  473. }
  474. public func deleteAllManuallyEnteredDoses(since startDate: Date, _ completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) {
  475. queue.async {
  476. var doseStoreError: DoseStore.DoseStoreError?
  477. self.cacheStore.managedObjectContext.performAndWait {
  478. do {
  479. let request: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  480. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "deletedAt == NIL"),
  481. NSPredicate(format: "startDate >= %@", startDate as NSDate),
  482. NSPredicate(format: "manuallyEntered == YES")])
  483. request.fetchBatchSize = 100
  484. let objects = try self.cacheStore.managedObjectContext.fetch(request)
  485. if !objects.isEmpty {
  486. let deletedAt = self.currentDate()
  487. for object in objects {
  488. object.deletedAt = deletedAt
  489. }
  490. doseStoreError = DoseStore.DoseStoreError(error: self.cacheStore.save())
  491. }
  492. }
  493. catch let error as NSError {
  494. doseStoreError = DoseStore.DoseStoreError(error: .coreDataError(error))
  495. }
  496. }
  497. self.handleUpdatedDoseData()
  498. self.delegate?.insulinDeliveryStoreHasUpdatedDoseData(self)
  499. completion(doseStoreError)
  500. }
  501. }
  502. }
  503. // MARK: - Cache Management
  504. extension InsulinDeliveryStore {
  505. var earliestCacheDate: Date {
  506. return currentDate(timeIntervalSinceNow: -cacheLength)
  507. }
  508. /// Purge all dose entries from the insulin delivery store and HealthKit (matching the specified device predicate).
  509. ///
  510. /// - Parameters:
  511. /// - healthKitPredicate: The HealthKit device predicate to match HealthKit insulin samples.
  512. /// - completion: The completion handler returning any error.
  513. public func purgeAllDoseEntries(healthKitPredicate: NSPredicate, completion: @escaping (Error?) -> Void) {
  514. queue.async {
  515. let storeError = self.purgeCachedInsulinDeliveryObjects(matching: nil)
  516. self.healthStore.deleteObjects(of: self.insulinQuantityType, predicate: healthKitPredicate) { _, _, healthKitError in
  517. self.queue.async {
  518. self.handleUpdatedDoseData()
  519. completion(storeError ?? healthKitError)
  520. }
  521. }
  522. }
  523. }
  524. private func purgeExpiredCachedInsulinDeliveryObjects() {
  525. purgeCachedInsulinDeliveryObjects(before: earliestCacheDate)
  526. }
  527. /// Purge cached insulin delivery objects from the insulin delivery store.
  528. ///
  529. /// - Parameters:
  530. /// - date: Purge cached insulin delivery objects with start date before this date.
  531. /// - completion: The completion handler returning any error.
  532. public func purgeCachedInsulinDeliveryObjects(before date: Date? = nil, completion: @escaping (Error?) -> Void) {
  533. queue.async {
  534. if let error = self.purgeCachedInsulinDeliveryObjects(before: date) {
  535. completion(error)
  536. return
  537. }
  538. self.handleUpdatedDoseData()
  539. completion(nil)
  540. }
  541. }
  542. @discardableResult
  543. private func purgeCachedInsulinDeliveryObjects(before date: Date? = nil) -> Error? {
  544. return purgeCachedInsulinDeliveryObjects(matching: date.map { NSPredicate(format: "endDate < %@", $0 as NSDate) })
  545. }
  546. private func purgeCachedInsulinDeliveryObjects(matching predicate: NSPredicate? = nil) -> Error? {
  547. dispatchPrecondition(condition: .onQueue(queue))
  548. var error: Error?
  549. cacheStore.managedObjectContext.performAndWait {
  550. do {
  551. let count = try cacheStore.managedObjectContext.purgeObjects(of: CachedInsulinDeliveryObject.self, matching: predicate)
  552. if count > 0 {
  553. self.log.default("Purged %d CachedInsulinDeliveryObjects", count)
  554. }
  555. } catch let coreDataError {
  556. self.log.error("Unable to purge CachedInsulinDeliveryObjects: %{public}@", String(describing: error))
  557. error = coreDataError
  558. }
  559. }
  560. return error
  561. }
  562. private func handleUpdatedDoseData() {
  563. dispatchPrecondition(condition: .onQueue(queue))
  564. self.purgeExpiredCachedInsulinDeliveryObjects()
  565. self.updateLastImmutableBasalEndDate()
  566. NotificationCenter.default.post(name: InsulinDeliveryStore.doseEntriesDidChange, object: self)
  567. }
  568. }
  569. // MARK: - Issue Report
  570. extension InsulinDeliveryStore {
  571. /// Generates a diagnostic report about the current state
  572. ///
  573. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  574. ///
  575. /// - parameter completion: The closure takes a single argument of the report string.
  576. public func generateDiagnosticReport(_ completion: @escaping (_ report: String) -> Void) {
  577. self.queue.async {
  578. var report: [String] = [
  579. "### InsulinDeliveryStore",
  580. "* cacheLength: \(self.cacheLength)",
  581. super.debugDescription,
  582. "* lastImmutableBasalEndDate: \(String(describing: self.lastImmutableBasalEndDate))",
  583. "",
  584. "#### cachedDoseEntries",
  585. ]
  586. switch self.getDoseEntries(start: Date(timeIntervalSinceNow: -.hours(24)), includeMutable: true) {
  587. case .failure(let error):
  588. report.append("Error: \(error)")
  589. case .success(let entries):
  590. for entry in entries {
  591. report.append(String(describing: entry))
  592. }
  593. }
  594. report.append("")
  595. completion(report.joined(separator: "\n"))
  596. }
  597. }
  598. }
  599. // MARK: - Query
  600. extension InsulinDeliveryStore {
  601. public struct QueryAnchor: Equatable, RawRepresentable {
  602. public typealias RawValue = [String: Any]
  603. internal var modificationCounter: Int64
  604. public init() {
  605. self.modificationCounter = 0
  606. }
  607. public init?(rawValue: RawValue) {
  608. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  609. return nil
  610. }
  611. self.modificationCounter = modificationCounter
  612. }
  613. public var rawValue: RawValue {
  614. var rawValue: RawValue = [:]
  615. rawValue["modificationCounter"] = modificationCounter
  616. return rawValue
  617. }
  618. }
  619. public enum DoseQueryResult {
  620. case success(QueryAnchor, [DoseEntry], [DoseEntry])
  621. case failure(Error)
  622. }
  623. public func executeDoseQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (DoseQueryResult) -> Void) {
  624. queue.async {
  625. var queryAnchor = queryAnchor ?? QueryAnchor()
  626. var queryCreatedResult = [DoseEntry]()
  627. var queryDeletedResult = [DoseEntry]()
  628. var queryError: Error?
  629. guard limit > 0 else {
  630. completion(.success(queryAnchor, [], []))
  631. return
  632. }
  633. self.cacheStore.managedObjectContext.performAndWait {
  634. let storedRequest: NSFetchRequest<CachedInsulinDeliveryObject> = CachedInsulinDeliveryObject.fetchRequest()
  635. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  636. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  637. storedRequest.fetchLimit = limit
  638. do {
  639. let stored = try self.cacheStore.managedObjectContext.fetch(storedRequest)
  640. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  641. queryAnchor.modificationCounter = modificationCounter
  642. }
  643. queryCreatedResult.append(contentsOf: stored.filter({ $0.deletedAt == nil }).compactMap { $0.dose })
  644. queryDeletedResult.append(contentsOf: stored.filter({ $0.deletedAt != nil }).compactMap { $0.dose })
  645. } catch let error {
  646. queryError = error
  647. }
  648. }
  649. if let queryError = queryError {
  650. completion(.failure(queryError))
  651. return
  652. }
  653. completion(.success(queryAnchor, queryCreatedResult, queryDeletedResult))
  654. }
  655. }
  656. }
  657. // MARK: - Unit Testing
  658. extension InsulinDeliveryStore {
  659. public var test_lastImmutableBasalEndDate: Date? {
  660. get {
  661. var date: Date?
  662. queue.sync {
  663. date = self.lastImmutableBasalEndDate
  664. }
  665. return date
  666. }
  667. set {
  668. queue.sync {
  669. self.lastImmutableBasalEndDate = newValue
  670. }
  671. }
  672. }
  673. }