|
@@ -50,50 +50,81 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
|
|
|
let context = makeContext()
|
|
let context = makeContext()
|
|
|
context.name = "storePumpEvents"
|
|
context.name = "storePumpEvents"
|
|
|
try await context.perform {
|
|
try await context.perform {
|
|
|
- for event in events {
|
|
|
|
|
- let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
|
|
|
|
|
- ofType: PumpEventStored.self,
|
|
|
|
|
- onContext: context,
|
|
|
|
|
- predicate: NSPredicate.duplicates(event.date),
|
|
|
|
|
- key: "timestamp",
|
|
|
|
|
- ascending: false,
|
|
|
|
|
- batchSize: 50
|
|
|
|
|
- ) as? [PumpEventStored] ?? []
|
|
|
|
|
|
|
+ // Batched de-duplication: fetch every already-stored event whose timestamp matches one of the
|
|
|
|
|
+ // incoming events in a single request, instead of one fetch per event (avoids an N+1 query).
|
|
|
|
|
+ // The (timestamp, type) key mirrors the uniqueness constraint on PumpEventStored, which acts as
|
|
|
|
|
+ // a race-safe backstop should a concurrent context insert the same event.
|
|
|
|
|
+ let incomingTimestamps = events.map { $0.date as NSDate }
|
|
|
|
|
+ let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
|
|
|
|
|
+ ofType: PumpEventStored.self,
|
|
|
|
|
+ onContext: context,
|
|
|
|
|
+ predicate: NSPredicate(format: "timestamp IN %@", incomingTimestamps),
|
|
|
|
|
+ key: "timestamp",
|
|
|
|
|
+ ascending: false,
|
|
|
|
|
+ batchSize: 50
|
|
|
|
|
+ ) as? [PumpEventStored] ?? []
|
|
|
|
|
+
|
|
|
|
|
+ // (timestamp, type) -> stored event, for O(1) look-up inside the loop. Newly created events are
|
|
|
|
|
+ // inserted here too, so duplicates within the same batch are caught as well.
|
|
|
|
|
+ func dedupKey(timestamp: Date?, type: String?) -> String {
|
|
|
|
|
+ "\(timestamp?.timeIntervalSinceReferenceDate ?? 0)|\(type ?? "")"
|
|
|
|
|
+ }
|
|
|
|
|
+ var existingByKey: [String: PumpEventStored] = [:]
|
|
|
|
|
+ for stored in existingEvents {
|
|
|
|
|
+ existingByKey[dedupKey(timestamp: stored.timestamp, type: stored.type)] = stored
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // Creates a bare PumpEventStored unless a (timestamp, type) duplicate already exists.
|
|
|
|
|
+ // Returns the new event, or nil if it was a duplicate.
|
|
|
|
|
+ @discardableResult func makeEventIfNew(timestamp: Date, type: PumpEvent) -> PumpEventStored? {
|
|
|
|
|
+ let key = dedupKey(timestamp: timestamp, type: type.rawValue)
|
|
|
|
|
+ guard existingByKey[key] == nil else {
|
|
|
|
|
+ debug(.coreData, "Duplicate event found with timestamp: \(timestamp)")
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
+ newPumpEvent.id = UUID().uuidString
|
|
|
|
|
+ newPumpEvent.timestamp = timestamp
|
|
|
|
|
+ newPumpEvent.type = type.rawValue
|
|
|
|
|
+ newPumpEvent.isUploadedToNS = false
|
|
|
|
|
+ newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
+ newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
+ existingByKey[key] = newPumpEvent
|
|
|
|
|
+ return newPumpEvent
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for event in events {
|
|
|
switch event.type {
|
|
switch event.type {
|
|
|
case .bolus:
|
|
case .bolus:
|
|
|
-
|
|
|
|
|
guard let dose = event.dose else { continue }
|
|
guard let dose = event.dose else { continue }
|
|
|
let amount = self.roundDose(
|
|
let amount = self.roundDose(
|
|
|
dose.unitsInDeliverableIncrements,
|
|
dose.unitsInDeliverableIncrements,
|
|
|
toIncrement: Double(self.settings.preferences.bolusIncrement)
|
|
toIncrement: Double(self.settings.preferences.bolusIncrement)
|
|
|
)
|
|
)
|
|
|
|
|
+ // restrict entry to now or past
|
|
|
|
|
+ let timestamp = event.date > Date() ? Date() : event.date
|
|
|
|
|
+ let key = dedupKey(timestamp: timestamp, type: PumpEvent.bolus.rawValue)
|
|
|
|
|
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
|
|
+ if let existingEvent = existingByKey[key] {
|
|
|
// Duplicate found, do not store the event
|
|
// Duplicate found, do not store the event
|
|
|
debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
|
|
|
- if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
|
|
|
|
|
- if existingEvent.timestamp == event.date {
|
|
|
|
|
- if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
|
|
|
|
|
- // Update existing event with new smaller value
|
|
|
|
|
- existingEvent.bolus?.amount = amount as NSDecimalNumber
|
|
|
|
|
- existingEvent.bolus?.isSMB = dose.automatic ?? true
|
|
|
|
|
- existingEvent.isUploadedToNS = false
|
|
|
|
|
- existingEvent.isUploadedToHealth = false
|
|
|
|
|
- existingEvent.isUploadedToTidepool = false
|
|
|
|
|
-
|
|
|
|
|
- debug(.coreData, "Updated existing event with smaller value: \(amount)")
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
|
|
|
|
|
+ // Update existing event with new smaller value (e.g. a cancelled / partial bolus)
|
|
|
|
|
+ existingEvent.bolus?.amount = amount as NSDecimalNumber
|
|
|
|
|
+ existingEvent.bolus?.isSMB = dose.automatic ?? true
|
|
|
|
|
+ existingEvent.isUploadedToNS = false
|
|
|
|
|
+ existingEvent.isUploadedToHealth = false
|
|
|
|
|
+ existingEvent.isUploadedToTidepool = false
|
|
|
|
|
+
|
|
|
|
|
+ debug(.coreData, "Updated existing event with smaller value: \(amount)")
|
|
|
}
|
|
}
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let newPumpEvent = PumpEventStored(context: context)
|
|
let newPumpEvent = PumpEventStored(context: context)
|
|
|
newPumpEvent.id = UUID().uuidString
|
|
newPumpEvent.id = UUID().uuidString
|
|
|
- // restrict entry to now or past
|
|
|
|
|
- newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
|
|
|
|
|
|
|
+ newPumpEvent.timestamp = timestamp
|
|
|
newPumpEvent.type = PumpEvent.bolus.rawValue
|
|
newPumpEvent.type = PumpEvent.bolus.rawValue
|
|
|
newPumpEvent.isUploadedToNS = false
|
|
newPumpEvent.isUploadedToNS = false
|
|
|
newPumpEvent.isUploadedToHealth = false
|
|
newPumpEvent.isUploadedToHealth = false
|
|
@@ -105,30 +136,21 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
|
|
|
newBolusEntry.isExternal = dose.manuallyEntered
|
|
newBolusEntry.isExternal = dose.manuallyEntered
|
|
|
newBolusEntry.isSMB = dose.automatic ?? true
|
|
newBolusEntry.isSMB = dose.automatic ?? true
|
|
|
|
|
|
|
|
|
|
+ existingByKey[key] = newPumpEvent
|
|
|
|
|
+
|
|
|
case .tempBasal:
|
|
case .tempBasal:
|
|
|
guard let dose = event.dose else { continue }
|
|
guard let dose = event.dose else { continue }
|
|
|
|
|
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
|
|
+ let delivered = dose.deliveredUnits
|
|
|
|
|
+ let isCancel = delivered != nil
|
|
|
|
|
+ guard !isCancel else { continue }
|
|
|
|
|
+
|
|
|
|
|
+ guard let newPumpEvent = makeEventIfNew(timestamp: event.date, type: .tempBasal) else {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let rate = Decimal(dose.unitsPerHour)
|
|
let rate = Decimal(dose.unitsPerHour)
|
|
|
let minutes = (dose.endDate - dose.startDate).timeInterval / 60
|
|
let minutes = (dose.endDate - dose.startDate).timeInterval / 60
|
|
|
- let delivered = dose.deliveredUnits
|
|
|
|
|
- let date = event.date
|
|
|
|
|
-
|
|
|
|
|
- let isCancel = delivered != nil
|
|
|
|
|
- guard !isCancel else { continue }
|
|
|
|
|
-
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.tempBasal.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
|
|
|
|
|
let newTempBasal = TempBasalStored(context: context)
|
|
let newTempBasal = TempBasalStored(context: context)
|
|
|
newTempBasal.pumpEvent = newPumpEvent
|
|
newTempBasal.pumpEvent = newPumpEvent
|
|
@@ -137,90 +159,24 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
|
|
|
newTempBasal.tempType = TempType.absolute.rawValue
|
|
newTempBasal.tempType = TempType.absolute.rawValue
|
|
|
|
|
|
|
|
case .suspend:
|
|
case .suspend:
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = event.date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
|
|
+ makeEventIfNew(timestamp: event.date, type: .pumpSuspend)
|
|
|
|
|
|
|
|
case .resume:
|
|
case .resume:
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = event.date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.pumpResume.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
|
|
+ makeEventIfNew(timestamp: event.date, type: .pumpResume)
|
|
|
|
|
|
|
|
case .rewind:
|
|
case .rewind:
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = event.date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.rewind.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
|
|
+ makeEventIfNew(timestamp: event.date, type: .rewind)
|
|
|
|
|
|
|
|
case .prime:
|
|
case .prime:
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = event.date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.prime.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
|
|
+ makeEventIfNew(timestamp: event.date, type: .prime)
|
|
|
|
|
|
|
|
case .alarm:
|
|
case .alarm:
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = event.date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
- newPumpEvent.note = event.title
|
|
|
|
|
|
|
+ let newPumpEvent = makeEventIfNew(timestamp: event.date, type: .pumpAlarm)
|
|
|
|
|
+ newPumpEvent?.note = event.title
|
|
|
|
|
|
|
|
case .replaceComponent(componentType: .infusionSet),
|
|
case .replaceComponent(componentType: .infusionSet),
|
|
|
.replaceComponent(componentType: .pump):
|
|
.replaceComponent(componentType: .pump):
|
|
|
- guard existingEvents.isEmpty else {
|
|
|
|
|
- // Duplicate found, do not store the event
|
|
|
|
|
- debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- let newPumpEvent = PumpEventStored(context: context)
|
|
|
|
|
- newPumpEvent.id = UUID().uuidString
|
|
|
|
|
- newPumpEvent.timestamp = event.date
|
|
|
|
|
- newPumpEvent.type = PumpEvent.siteChange.rawValue
|
|
|
|
|
- newPumpEvent.isUploadedToNS = false
|
|
|
|
|
- newPumpEvent.isUploadedToHealth = false
|
|
|
|
|
- newPumpEvent.isUploadedToTidepool = false
|
|
|
|
|
|
|
+ makeEventIfNew(timestamp: event.date, type: .siteChange)
|
|
|
|
|
|
|
|
default:
|
|
default:
|
|
|
continue
|
|
continue
|