Procházet zdrojové kódy

Merge branch 'core-data_stable' of github.com:polscm32/Open-iAPS into core-data_stable

dnzxy před 2 roky
rodič
revize
e514e07643
29 změnil soubory, kde provedl 322 přidání a 185 odebrání
  1. 4 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 31 9
      FreeAPS/Sources/APS/APSManager.swift
  3. 1 1
      FreeAPS/Sources/APS/DeviceDataManager.swift
  4. 1 1
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  5. 1 1
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  6. 1 1
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  7. 1 1
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  8. 6 1
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  9. 26 6
      FreeAPS/Sources/Application/FreeAPSApp.swift
  10. 1 1
      FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift
  11. 1 1
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  12. 1 1
      FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  13. 2 1
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  14. 23 4
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  15. 95 74
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  16. 7 3
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  17. 1 1
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  18. 1 1
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  19. 1 1
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  20. 1 1
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  21. 1 1
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  22. 1 1
      FreeAPS/Sources/Shortcuts/BaseIntentsRequest.swift
  23. 1 1
      FreeAPS/Sources/Shortcuts/State/StateIntentRequest.swift
  24. 73 33
      Model/CoreDataStack.swift
  25. 1 31
      Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  26. 35 0
      Model/Helper/CoreDataError.swift
  27. 1 1
      Model/Helper/CoreDataStorage.swift
  28. 2 2
      Model/Helper/GlucoseStored+helper.swift
  29. 1 5
      PumpEventStored+CoreDataProperties.swift

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -310,6 +310,7 @@
 		5825D15D2BD4058F00F36E9B /* Target+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D12F2BD4058F00F36E9B /* Target+CoreDataProperties.swift */; };
 		5825D15E2BD4058F00F36E9B /* Protein+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1302BD4058F00F36E9B /* Protein+CoreDataClass.swift */; };
 		5825D15F2BD4058F00F36E9B /* Protein+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825D1312BD4058F00F36E9B /* Protein+CoreDataProperties.swift */; };
+		582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582FAE422C05102C00D1C13F /* CoreDataError.swift */; };
 		583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684052BD178DB00070A60 /* GlucoseStored+helper.swift */; };
 		583684082BD195A700070A60 /* Determination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583684072BD195A700070A60 /* Determination.swift */; };
 		5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+helper.swift */; };
@@ -936,6 +937,7 @@
 		5825D12F2BD4058F00F36E9B /* Target+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Target+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		5825D1302BD4058F00F36E9B /* Protein+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Protein+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		5825D1312BD4058F00F36E9B /* Protein+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Protein+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		582FAE422C05102C00D1C13F /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = "<group>"; };
 		583684052BD178DB00070A60 /* GlucoseStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStored+helper.swift"; sourceTree = "<group>"; };
 		583684072BD195A700070A60 /* Determination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Determination.swift; sourceTree = "<group>"; };
 		5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+helper.swift"; sourceTree = "<group>"; };
@@ -2233,6 +2235,7 @@
 				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
 				581AC4382BE22ED10038760C /* JSONConverter.swift */,
 				BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */,
+				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -3005,6 +3008,7 @@
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
 				5825D1462BD4058F00F36E9B /* Oref0Suggestion+CoreDataClass.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
+				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
 				190EBCC629FF138000BA767D /* StatConfigProvider.swift in Sources */,
 				38E98A2725F52C9300C0CED0 /* CollectionIssueReporter.swift in Sources */,

+ 31 - 9
FreeAPS/Sources/APS/APSManager.swift

@@ -81,9 +81,10 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private var cleanupTimer: Timer?
     @Persisted(key: "lastHistoryCleanupDate") private var lastHistoryCleanupDate = Date.distantPast
+    @Persisted(key: "lastPurgeDate") private var lastPurgeDate = Date.distantPast
 
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    let privateContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let privateContext = CoreDataStack.shared.newTaskContext()
 
     private var openAPS: OpenAPS!
 
@@ -148,18 +149,39 @@ final class BaseAPSManager: APSManager, Injectable {
         let calendar = Calendar.current
 
         // Check if last clean is longer than one day ago
-        if calendar.isDate(now, inSameDayAs: lastHistoryCleanupDate) {
-            // Cleanup already done
-            return
+        if !calendar.isDateInToday(lastHistoryCleanupDate) {
+            // Perform daily cleanup
+            Task {
+                await CoreDataStack.shared.cleanupPersistentHistoryTokens(before: Date.oneWeekAgo)
+                // Update lastHistoryCleanupDate only if cleanup was successful
+                lastHistoryCleanupDate = now
+            }
         }
 
-        // Cleanup
-        Task {
-            await CoreDataStack.shared.cleanupPersistentHistory(before: Date.oneWeekAgo)
+        // Check if last purge is longer than one week ago
+        if let lastPurge = calendar.date(byAdding: .day, value: 7, to: lastPurgeDate), now >= lastPurge {
+            // Perform weekly purge
+            Task {
+                do {
+                    try await purgeOldNSManagedObjects()
+                    // Update lastPurgeDate only if purge was successful
+                    lastPurgeDate = now
+                } catch {
+                    debugPrint("Failed to purge old managed objects: \(error.localizedDescription)")
+                }
+            }
         }
+    }
+
+    private func purgeOldNSManagedObjects() async throws {
+        try await CoreDataStack.shared.batchDeleteOlderThan(GlucoseStored.self, dateKey: "date", days: 90)
+        try await CoreDataStack.shared.batchDeleteOlderThan(PumpEventStored.self, dateKey: "timestamp", days: 90)
+        try await CoreDataStack.shared.batchDeleteOlderThan(OrefDetermination.self, dateKey: "deliverAt", days: 90)
+        try await CoreDataStack.shared.batchDeleteOlderThan(OpenAPS_Battery.self, dateKey: "date", days: 90)
+        try await CoreDataStack.shared.batchDeleteOlderThan(CarbEntryStored.self, dateKey: "date", days: 90)
+        try await CoreDataStack.shared.batchDeleteOlderThan(Forecast.self, dateKey: "date", days: 90)
 
-        // Update lastHistoryCleanupDate
-        lastHistoryCleanupDate = now
+        // TODO: - Purge Data of other (future) entities as well
     }
 
     private func subscribe() {

+ 1 - 1
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -73,7 +73,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
     private var pumpUpdatePromise: Future<Bool, Never>.Promise?
     @SyncAccess var loopInProgress: Bool = false
-    private let privateContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    private let privateContext = CoreDataStack.shared.newTaskContext()
 
     var pumpManager: PumpManagerUI? {
         didSet {

+ 1 - 1
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -32,7 +32,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     private lazy var dexcomSourceG7 = DexcomSourceG7(glucoseStorage: glucoseStorage, glucoseManager: self)
     private lazy var simulatorSource = GlucoseSimulatorSource()
 
-    private let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    private let context = CoreDataStack.shared.newTaskContext()
 
     init(resolver: Resolver) {
         injectServices(resolver)

+ 1 - 1
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -9,7 +9,7 @@ final class OpenAPS {
 
     private let storage: FileStorage
 
-    let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let context = CoreDataStack.shared.newTaskContext()
 
     let jsonConverter = JSONConverter()
 

+ 1 - 1
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -21,7 +21,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settings: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let coredataContext = CoreDataStack.shared.newTaskContext()
 
     init(resolver: Resolver) {
         injectServices(resolver)

+ 1 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -24,7 +24,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settingsManager: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let coredataContext = CoreDataStack.shared.newTaskContext()
 
     private enum Config {
         static let filterTime: TimeInterval = 3.5 * 60

+ 6 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -31,7 +31,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     typealias PumpEvent = PumpEventStored.EventType
     typealias TempType = PumpEventStored.TempType
 
-    private let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    private let context = CoreDataStack.shared.newTaskContext()
+
+    private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
+        let roundedValue = (dose / increment).rounded() * increment
+        return Decimal(roundedValue)
+    }
 
     private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
         let roundedValue = (dose / increment).rounded() * increment

+ 26 - 6
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -58,25 +58,45 @@ import Swinject
         )
         loadServices()
 
-        // Clear the persistentHistory every time the app starts
-        let coreDataStack = self.coreDataStack
-        Task {
-            await coreDataStack.cleanupPersistentHistory(before: Date.oneWeekAgo)
-        }
+        // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
+        cleanupOldData()
     }
 
     var body: some Scene {
         WindowGroup {
             Main.RootView(resolver: resolver)
-                .environment(\.managedObjectContext, CoreDataStack.shared.persistentContainer.viewContext)
+                .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)
         }
         .onChange(of: scenePhase) { newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")
+
+            /// If the App goes to the background we should ensure that all the changes are saved from the viewContext to the Persistent Container
+            if newScenePhase == .background {
+                coreDataStack.save()
+            }
+        }
+    }
+
+    private func cleanupOldData() {
+        Task {
+            await coreDataStack.cleanupPersistentHistoryTokens(before: Date.oneWeekAgo)
+            try await purgeOldNSManagedObjects()
         }
     }
 
+    private func purgeOldNSManagedObjects() async throws {
+        try await coreDataStack.batchDeleteOlderThan(GlucoseStored.self, dateKey: "date", days: 90)
+        try await coreDataStack.batchDeleteOlderThan(PumpEventStored.self, dateKey: "timestamp", days: 90)
+        try await coreDataStack.batchDeleteOlderThan(OrefDetermination.self, dateKey: "deliverAt", days: 90)
+        try await coreDataStack.batchDeleteOlderThan(OpenAPS_Battery.self, dateKey: "date", days: 90)
+        try await coreDataStack.batchDeleteOlderThan(CarbEntryStored.self, dateKey: "date", days: 90)
+        try await coreDataStack.batchDeleteOlderThan(Forecast.self, dateKey: "date", days: 90)
+
+        // TODO: - Purge Data of other (future) entities as well
+    }
+
     private func handleURL(_ url: URL) {
         let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
 

+ 1 - 1
FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift

@@ -6,7 +6,7 @@ extension AddTempTarget {
         @Injected() private var storage: TempTargetsStorage!
         @Injected() var apsManager: APSManager!
 
-        let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+        let coredataContext = CoreDataStack.shared.newTaskContext()
 
         @Published var low: Decimal = 0
         // @Published var target: Decimal = 0

+ 1 - 1
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -97,7 +97,7 @@ extension Bolus {
         let now = Date.now
 
         let context = CoreDataStack.shared.persistentContainer.viewContext
-        let backgroundContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+        let backgroundContext = CoreDataStack.shared.newTaskContext()
 
         typealias PumpEvent = PumpEventStored.EventType
 

+ 1 - 1
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -16,7 +16,7 @@ extension Calibrations {
         var units: GlucoseUnits = .mmolL
 
         // TODO: - test if we need to use the viewContext here
-        private let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+        private let context = CoreDataStack.shared.newTaskContext()
 
         override func subscribe() {
             units = settingsManager.settings.units

+ 2 - 1
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 import SwiftUI
 
@@ -218,5 +219,5 @@ enum DataTable {
 
 protocol DataTableProvider: Provider {
     func deleteCarbs(_ treatment: CarbEntryStored)
-    func deleteInsulin(_ treatment: PumpEventStored)
+    func deleteInsulin(with treatmentObjectID: NSManagedObjectID)
 }

+ 23 - 4
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 
 extension DataTable {
@@ -16,10 +17,28 @@ extension DataTable {
 //            nightscoutManager.deleteCarbs(treatment, complexMeal: false)
         }
 
-        func deleteInsulin(_ treatment: PumpEventStored) {
-            nightscoutManager.deleteInsulin(at: treatment.timestamp ?? Date())
-            let id = treatment.id
-            healthkitManager.deleteInsulin(syncID: id)
+        func deleteInsulin(with treatmentObjectID: NSManagedObjectID) {
+            let taskContext = CoreDataStack.shared.newTaskContext()
+
+            taskContext.perform {
+                do {
+                    guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
+                    else {
+                        debug(.default, "Could not cast the object to PumpEventStored")
+                        return
+                    }
+                    self.nightscoutManager.deleteInsulin(at: treatmentToDelete.timestamp ?? Date())
+                    let id = treatmentToDelete.id
+                    self.healthkitManager.deleteInsulin(syncID: id)
+
+                    taskContext.delete(treatmentToDelete)
+                    try taskContext.save()
+
+                    debug(.default, "Successfully deleted the treatment object.")
+                } catch {
+                    debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
+                }
+            }
         }
 
         func deleteManualGlucose(date: Date?) {

+ 95 - 74
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -10,7 +10,7 @@ extension DataTable {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var healthKitManager: HealthKitManager!
 
-        let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
+        let coredataContext = CoreDataStack.shared.newTaskContext()
 
         @Published var mode: Mode = .treatments
         @Published var treatments: [Treatment] = []
@@ -31,114 +31,135 @@ extension DataTable {
             broadcaster.register(DeterminationObserver.self, observer: self)
         }
 
-        @MainActor func invokeGlucoseDeletionTask(_ glucose: GlucoseStored) {
+        // Carb and FPU deletion from history
+        /// marked as MainActor to be able to publish changes from the background
+        /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        @MainActor func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
+                await deleteGlucose(treatmentObjectID)
+            }
+        }
+
+        func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+            let taskContext = CoreDataStack.shared.newTaskContext()
+            taskContext.name = "deleteContext"
+            taskContext.transactionAuthor = "deleteGlucose"
+
+            var glucose: GlucoseStored?
+
+            await taskContext.perform {
                 do {
-                    await deleteGlucose(glucose)
-                    provider.deleteManualGlucose(date: glucose.date)
+                    glucose = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
+
+                    guard let glucoseToDelete = glucose else {
+                        debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
+                        return
+                    }
+
+                    taskContext.delete(glucoseToDelete)
+
+                    guard taskContext.hasChanges else { return }
+                    try taskContext.save()
+                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
+                } catch {
+                    debugPrint(
+                        "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
+                    )
                 }
             }
-        }
 
-        func deleteGlucose(_ glucose: GlucoseStored) async {
-            do {
-                coredataContext.delete(glucose)
-                try coredataContext.save()
-                debugPrint(
-                    "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data"
-                )
-            } catch {
+            guard let glucoseToDelete = glucose else {
                 debugPrint(
-                    "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data"
+                    "Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found after task context execution"
                 )
+                return
             }
+
+            provider.deleteManualGlucose(date: glucoseToDelete.date)
         }
 
-        @MainActor func invokeCarbDeletionTask(_ treatment: CarbEntryStored) {
+        // Carb and FPU deletion from history
+        /// marked as MainActor to be able to publish changes from the background
+        /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        @MainActor func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
-                do {
-                    await deleteCarbs(treatment)
-                    carbEntryDeleted = true
-                    waitForSuggestion = true
-                }
+                await deleteCarbs(treatmentObjectID)
+                carbEntryDeleted = true
+                waitForSuggestion = true
             }
         }
 
-        func deleteCarbs(_ carbEntry: CarbEntryStored) async {
-            if carbEntry.isFPU, let fpuID = carbEntry.id {
-                // fetch request for all carb entries with the same id
-                let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                fetchRequest.predicate = NSPredicate(format: "id == %@", fpuID as CVarArg)
+        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+            let taskContext = CoreDataStack.shared.newTaskContext()
+            taskContext.name = "deleteContext"
+            taskContext.transactionAuthor = "deleteCarbs"
 
-                // NSBatchDeleteRequest
-                let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
-                deleteRequest.resultType = .resultTypeCount
+            var carbEntry: CarbEntryStored?
 
+            await taskContext.perform {
                 do {
-                    // execute the batch delete request
-                    let result = try coredataContext.execute(deleteRequest) as? NSBatchDeleteResult
-                    debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
-
-                    // merge changes from the database operation into the main context
-                    if let objectIDs = (result?.result as? [NSManagedObjectID]) {
-                        NSManagedObjectContext.mergeChanges(
-                            fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs],
-                            into: [coredataContext]
-                        )
+                    carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                    guard let carbEntry = carbEntry else {
+                        debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
+                        return
                     }
 
-                    try coredataContext.save()
+                    if carbEntry.isFPU, let fpuID = carbEntry.id {
+                        // fetch request for all carb entries with the same id
+                        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
+                        fetchRequest.predicate = NSPredicate(format: "id == %@", fpuID as CVarArg)
+
+                        // NSBatchDeleteRequest
+                        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
+                        deleteRequest.resultType = .resultTypeCount
+
+                        // execute the batch delete request
+                        let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
+                        debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
+
+                        guard taskContext.hasChanges else { return }
+                        try taskContext.save()
+
+                    } else {
+                        taskContext.delete(carbEntry)
+
+                        guard taskContext.hasChanges else { return }
+                        try taskContext.save()
+                        debugPrint(
+                            "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
+                        )
+                    }
 
-                    provider.deleteCarbs(carbEntry)
-                    apsManager.determineBasalSync()
-                } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting FPU entries: \(error.localizedDescription)")
-                }
-            } else {
-                do {
-                    coredataContext.delete(carbEntry)
-                    try coredataContext.save()
-                    debugPrint(
-                        "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
-                    )
                 } catch {
-                    debugPrint(
-                        "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting carb entry from core data"
-                    )
+                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
                 }
+            }
 
+            // Delete carbs also from Nightscout and perform a determine basal sync to update cob
+            if let carbEntry = carbEntry {
                 provider.deleteCarbs(carbEntry)
                 apsManager.determineBasalSync()
             }
         }
 
-        @MainActor func invokeInsulinDeletionTask(_ treatment: PumpEventStored) {
+        // Insulin deletion from history
+        /// marked as MainActor to be able to publish changes from the background
+        /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        @MainActor func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
-                do {
-                    await deleteInsulin(treatment)
-                    insulinEntryDeleted = true
-                    waitForSuggestion = true
-                }
+                await deleteInsulin(treatmentObjectID)
+                insulinEntryDeleted = true
+                waitForSuggestion = true
             }
         }
 
-        func deleteInsulin(_ treatment: PumpEventStored) async {
+        func deleteInsulin(_ treatmentObjectID: NSManagedObjectID) async {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
-                    do {
-                        coredataContext.delete(treatment)
-                        try coredataContext.save()
-                        debugPrint(
-                            "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted insulin from core data"
-                        )
-                    } catch {
-                        debugPrint(
-                            "Data Table State: \(#function) \(DebuggingIdentifiers.failed) error while deleting insulin from core data"
-                        )
-                    }
+                    CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
-                    provider.deleteInsulin(treatment)
+                    provider.deleteInsulin(with: treatmentObjectID)
                     apsManager.determineBasalSync()
                 } else {
                     print("authentication failed")
@@ -169,7 +190,7 @@ extension DataTable {
             // TODO: -do we need this?
             // Save to Health
             var saveToHealth = [BloodGlucose]()
-            saveToHealth.append(saveToJSON)
+//            saveToHealth.append(saveToJSON)
 
             // save to core data
             coredataContext.perform {

+ 7 - 3
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -265,7 +265,8 @@ extension DataTable {
                                     debug(.default, "Cannot gracefully unwrap alertCarbEntryToDelete!")
                                     return
                                 }
-                                state.invokeGlucoseDeletionTask(glucoseToDelete)
+                                let glucoseToDeleteObjectID = glucoseToDelete.objectID
+                                state.invokeGlucoseDeletionTask(glucoseToDeleteObjectID)
                             }
                         } message: {
                             Text("\n" + NSLocalizedString(alertMessage, comment: ""))
@@ -425,8 +426,9 @@ extension DataTable {
                         debug(.default, "Cannot gracefully unwrap alertTreatmentToDelete!")
                         return
                     }
+                    let treatmentObjectID = treatmentToDelete.objectID
 
-                    state.invokeInsulinDeletionTask(treatmentToDelete)
+                    state.invokeInsulinDeletionTask(treatmentObjectID)
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))
@@ -485,7 +487,9 @@ extension DataTable {
                         debug(.default, "Cannot gracefully unwrap alertCarbEntryToDelete!")
                         return
                     }
-                    state.invokeCarbDeletionTask(carbEntryToDelete)
+                    let treatmentObjectID = carbEntryToDelete.objectID
+
+                    state.invokeCarbDeletionTask(treatmentObjectID)
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -68,7 +68,7 @@ extension Home {
 
         @Published var waitForSuggestion: Bool = false
 
-        let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+        let context = CoreDataStack.shared.newTaskContext()
 
         override func subscribe() {
             setupBasals()

+ 1 - 1
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -16,7 +16,7 @@ extension NightscoutConfig {
         @Injected() private var storage: FileStorage!
         @Injected() var apsManager: APSManager!
 
-        let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+        let coredataContext = CoreDataStack.shared.newTaskContext()
 
         @Published var url = ""
         @Published var secret = ""

+ 1 - 1
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -56,7 +56,7 @@ extension OverrideProfilesConfig {
             maxValue = settingsManager.preferences.autosensMax
         }
 
-        let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+        let coredataContext = CoreDataStack.shared.newTaskContext()
 
         func initialFetchForProfilePresets() -> [OverridePresets] {
             let fr = OverridePresets.fetchRequest()

+ 1 - 1
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -62,7 +62,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         return NightscoutAPI(url: url, secret: secret)
     }
 
-    private let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    private let context = CoreDataStack.shared.newTaskContext()
 
     private var lastTwoDeterminations: [OrefDetermination]?
 

+ 1 - 1
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -19,7 +19,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     @Injected() private var garmin: GarminManager!
 
     let coreDataStorage = CoreDataStorage()
-    let context = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let context = CoreDataStack.shared.newTaskContext()
 
     private var lifetime = Lifetime()
 

+ 1 - 1
FreeAPS/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -16,7 +16,7 @@ import Swinject
 
     let resolver: Resolver
 
-    let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let coredataContext = CoreDataStack.shared.newTaskContext()
 
     override init() {
         resolver = FreeAPSApp.resolver

+ 1 - 1
FreeAPS/Sources/Shortcuts/State/StateIntentRequest.swift

@@ -55,7 +55,7 @@ enum StateIntentError: Error {
 }
 
 @available(iOS 16.0, *) final class StateIntentRequest: BaseIntentsRequest {
-    let moc = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let moc = CoreDataStack.shared.newTaskContext()
 
     func getLastGlucose(onContext: NSManagedObjectContext) throws
         -> (dateGlucose: Date, glucose: String, trend: String, delta: String)

+ 73 - 33
Model/CoreDataStack.swift

@@ -78,7 +78,7 @@ class CoreDataStack: ObservableObject {
     }()
 
     /// Creates and configures a private queue context
-    private func newTaskContext() -> NSManagedObjectContext {
+    func newTaskContext() -> NSManagedObjectContext {
         // Create a private queue context
         /// - Tag: newBackgroundContext
         let taskContext = persistentContainer.newBackgroundContext()
@@ -130,14 +130,14 @@ class CoreDataStack: ObservableObject {
 
     // Clean old Persistent History
     /// - Tag: clearHistory
-    func cleanupPersistentHistory(before date: Date) async {
+    func cleanupPersistentHistoryTokens(before date: Date) async {
         let taskContext = newTaskContext()
-        taskContext.name = "cleanPersistentHistoryContext"
+        taskContext.name = "cleanPersistentHistoryTokensContext"
 
         await taskContext.perform {
-            let deleteHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
+            let deleteHistoryTokensRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
             do {
-                try taskContext.execute(deleteHistoryRequest)
+                try taskContext.execute(deleteHistoryTokensRequest)
                 debugPrint("\(DebuggingIdentifiers.succeeded) Successfully deleted persistent history before \(date)")
             } catch {
                 debugPrint(
@@ -151,46 +151,71 @@ class CoreDataStack: ObservableObject {
 // MARK: - Delete
 
 extension CoreDataStack {
-    /// Synchronously delete entries with specified object IDs
+    /// Synchronously delete entry with specified object IDs
     ///  - Tag: synchronousDelete
-    func deleteObject(identifiedBy objectIDs: [NSManagedObjectID]) {
+    func deleteObject(identifiedBy objectID: NSManagedObjectID) {
         let viewContext = persistentContainer.viewContext
         debugPrint("Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
 
         viewContext.perform {
-            objectIDs.forEach { objectID in
+            do {
                 let entryToDelete = viewContext.object(with: objectID)
                 viewContext.delete(entryToDelete)
+
+                guard viewContext.hasChanges else { return }
+                try viewContext.save()
+                debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
+            } catch {
+                debugPrint("Failed to delete data: \(error.localizedDescription)")
             }
         }
-
-        debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
     }
 
-    /// Asynchronously deletes records
+    /// Asynchronously deletes records for entities
     ///  - Tag: batchDelete
-//    func batchDelete<T: NSManagedObject>(_ objects: [T]) async throws {
-//        let objectIDs = objects.map(\.objectID)
-//        let taskContext = newTaskContext()
-//        // Add name and author to identify source of persistent history changes.
-//        taskContext.name = "deleteContext"
-//        taskContext.transactionAuthor = "batchDelete"
-//        debugPrint("Start deleting data from the store... \(DebuggingIdentifiers.inProgress)")
-//
-//        try await taskContext.perform {
-//            // Execute the batch delete.
-//            let batchDeleteRequest = NSBatchDeleteRequest(objectIDs: objectIDs)
-//            guard let fetchResult = try? taskContext.execute(batchDeleteRequest),
-//                  let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
-//                  let success = batchDeleteResult.result as? Bool, success
-//            else {
-//                debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-//                throw CoreDataError.batchDeleteError
-//            }
-//        }
-//
-//        debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
-//    }
+    func batchDeleteOlderThan<T: NSManagedObject>(_ objectType: T.Type, dateKey: String, days: Int) async throws {
+        let taskContext = newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "batchDelete"
+
+        // Get the number of days we want to keep the data
+        let targetDate = Calendar.current.date(byAdding: .day, value: -days, to: Date())!
+
+        // Fetch all the objects that are older than the specified days
+        let fetchRequest = NSFetchRequest<NSManagedObjectID>(entityName: String(describing: objectType))
+        fetchRequest.predicate = NSPredicate(format: "%K < %@", dateKey, targetDate as NSDate)
+        fetchRequest.resultType = .managedObjectIDResultType
+
+        do {
+            // Execute the Fetch Request
+            let objectIDs = try await taskContext.perform {
+                try taskContext.fetch(fetchRequest)
+            }
+
+            // Guard check if there are NSManagedObjects older than 90 days
+            guard !objectIDs.isEmpty else {
+                debugPrint("No objects found older than \(days) days.")
+                return
+            }
+
+            // Execute the Batch Delete
+            try await taskContext.perform {
+                let batchDeleteRequest = NSBatchDeleteRequest(objectIDs: objectIDs)
+                guard let fetchResult = try? taskContext.execute(batchDeleteRequest),
+                      let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
+                      let success = batchDeleteResult.result as? Bool, success
+                else {
+                    debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
+                    throw CoreDataError.batchDeleteError
+                }
+            }
+
+            debugPrint("Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
+        } catch {
+            debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            throw CoreDataError.batchDeleteError
+        }
+    }
 }
 
 // MARK: - Fetch Requests
@@ -372,6 +397,21 @@ extension CoreDataStack {
 
 // MARK: - Save
 
+/// This function is used when terminating the App to ensure any unsaved changes on the view context made their way to the persistent container
+extension CoreDataStack {
+    func save() {
+        let context = persistentContainer.viewContext
+
+        guard context.hasChanges else { return }
+
+        do {
+            try context.save()
+        } catch {
+            debugPrint("Error saving context \(DebuggingIdentifiers.failed): \(error)")
+        }
+    }
+}
+
 extension NSManagedObjectContext {
     // takes a context as a parameter to be executed either on the main thread or on a background thread
     /// - Tag: save

+ 1 - 31
Model/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,30 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E224" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
-    <entity name="Autosens_" representedClassName="Autosens_" syncable="YES">
-        <attribute name="newisf" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="ratio" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-    </entity>
-    <entity name="Autotune_" representedClassName="Autotune_" syncable="YES">
-        <attribute name="basalProfile" optional="YES" attributeType="Transformable"/>
-        <attribute name="carbRatio" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-        <attribute name="sensitivity" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-    </entity>
-    <entity name="BGaverages" representedClassName="BGaverages" syncable="YES">
-        <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="average_7" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="average_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-    </entity>
-    <entity name="BGmedian" representedClassName="BGmedian" syncable="YES">
-        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-        <attribute name="median" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="median_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="median_7" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-        <attribute name="median_30" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-    </entity>
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G120" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -204,11 +179,6 @@
         <attribute name="fat" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="protein" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
     </entity>
-    <entity name="Protein" representedClassName="Protein" syncable="YES">
-        <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-        <attribute name="enteredBy" optional="YES" attributeType="String"/>
-        <attribute name="protein" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
-    </entity>
     <entity name="PumpEventStored" representedClassName="PumpEventStored" syncable="YES">
         <attribute name="id_" optional="YES" attributeType="String"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>

+ 35 - 0
Model/Helper/CoreDataError.swift

@@ -0,0 +1,35 @@
+import Foundation
+
+enum CoreDataError: Error {
+    case creationError
+    case batchInsertError
+    case batchDeleteError
+    case persistentHistoryChangeError
+    case unexpectedError(error: Error)
+    case fetchError
+}
+
+extension CoreDataError: LocalizedError {
+    var errorDescription: String? {
+        switch self {
+        case .creationError:
+            return NSLocalizedString("Failed to create a new object.", comment: "")
+        case .batchInsertError:
+            return NSLocalizedString("Failed to execute a batch insert request.", comment: "")
+        case .batchDeleteError:
+            return NSLocalizedString("Failed to execute a batch delete request.", comment: "")
+        case .persistentHistoryChangeError:
+            return NSLocalizedString("Failed to execute a persistent history change request.", comment: "")
+        case let .unexpectedError(error):
+            return NSLocalizedString("Received unexpected error. \(error.localizedDescription)", comment: "")
+        case .fetchError:
+            return NSLocalizedString("Failed to fetch object \(DebuggingIdentifiers.failed).", comment: "")
+        }
+    }
+}
+
+extension CoreDataError: Identifiable {
+    var id: String? {
+        errorDescription
+    }
+}

+ 1 - 1
Model/Helper/CoreDataStorage.swift

@@ -4,7 +4,7 @@ import SwiftDate
 import Swinject
 
 final class CoreDataStorage {
-    let coredataContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
+    let coredataContext = CoreDataStack.shared.newTaskContext()
 
     func fetchLatestOverride() -> [Override] {
         var overrideArray = [Override]()

+ 2 - 2
Model/Helper/GlucoseStored+helper.swift

@@ -21,9 +21,9 @@ extension GlucoseStored {
     }
 
     static func glucoseIsFlat(_ glucose: [GlucoseStored]) -> Bool {
-        guard glucose.count >= 3 else { return false }
+        guard glucose.count >= 4 else { return false }
 
-        let lastThreeValues = glucose.suffix(3)
+        let lastThreeValues = glucose.suffix(4)
         let firstValue = lastThreeValues.last?.glucose
 
         return lastThreeValues.allSatisfy { $0.glucose == firstValue }

+ 1 - 5
PumpEventStored+CoreDataProperties.swift

@@ -17,10 +17,6 @@ extension PumpEventStored: Identifiable {}
 
 public extension PumpEventStored {
     var id: String {
-        #if DEBUG
-            return id_!
-        #else
-            return id_ ?? UUID().uuidString
-        #endif
+        id_ ?? UUID().uuidString
     }
 }