浏览代码

clear old persistent history

polscm32 2 年之前
父节点
当前提交
091619730c

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -382,6 +382,7 @@
 		BDB3C1032C0341E600CEEAA1 /* PumpEventStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FD2C0341E500CEEAA1 /* PumpEventStored+CoreDataProperties.swift */; };
 		BDB3C1032C0341E600CEEAA1 /* PumpEventStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FD2C0341E500CEEAA1 /* PumpEventStored+CoreDataProperties.swift */; };
 		BDB3C1042C0341E600CEEAA1 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */; };
 		BDB3C1042C0341E600CEEAA1 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */; };
 		BDB3C1052C0341E600CEEAA1 /* TempBasalStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */; };
 		BDB3C1052C0341E600CEEAA1 /* TempBasalStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */; };
+		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
 		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
@@ -1006,6 +1007,7 @@
 		BDB3C0FD2C0341E500CEEAA1 /* PumpEventStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpEventStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FD2C0341E500CEEAA1 /* PumpEventStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpEventStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FE2C0341E500CEEAA1 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BDB3C0FF2C0341E500CEEAA1 /* TempBasalStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
 		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
 		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
@@ -2230,6 +2232,7 @@
 				CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */,
 				CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */,
 				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
 				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
 				581AC4382BE22ED10038760C /* JSONConverter.swift */,
 				581AC4382BE22ED10038760C /* JSONConverter.swift */,
+				BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */,
 			);
 			);
 			path = Helper;
 			path = Helper;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -3068,6 +3071,7 @@
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */,
 				190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
+				BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,

+ 41 - 0
FreeAPS/Sources/APS/APSManager.swift

@@ -79,6 +79,9 @@ final class BaseAPSManager: APSManager, Injectable {
         }
         }
     }
     }
 
 
+    private var cleanupTimer: Timer?
+    @Persisted(key: "lastHistoryCleanupDate") private var lastHistoryCleanupDate = Date.distantPast
+
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     let privateContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
     let privateContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
 
 
@@ -129,6 +132,44 @@ final class BaseAPSManager: APSManager, Injectable {
         isLooping
         isLooping
             .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
             .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
             .store(in: &lifetime)
             .store(in: &lifetime)
+        startCleanupTimer()
+    }
+
+    private func startCleanupTimer() {
+        // Call the timer once every 12 hours to ensure that no clean gets missed
+        cleanupTimer = Timer.scheduledTimer(withTimeInterval: 12 * 60 * 60, repeats: true) { [weak self] _ in
+            self?.performCleanupIfNeeded()
+        }
+        RunLoop.current.add(cleanupTimer!, forMode: .common)
+    }
+
+    private func performCleanupIfNeeded() {
+        let now = Date()
+        let calendar = Calendar.current
+
+        // Check if there was already a clean on the specified day
+        if calendar.isDate(now, inSameDayAs: lastHistoryCleanupDate) {
+            // Cleanup was already done
+            return
+        }
+
+        DispatchQueue.global(qos: .background).async {
+            // Logging start time
+            let startTime = Date()
+            print("Starting cleanup at \(startTime)")
+
+            // Cleanup
+            CoreDataStack.shared.cleanupPersistentHistory(before: Date.oneWeekAgo)
+
+            // Logging end time
+            let endTime = Date()
+            print("Finished cleanup at \(endTime), duration: \(endTime.timeIntervalSince(startTime)) seconds")
+
+            // Update last cleanup date
+            DispatchQueue.main.async {
+                self.lastHistoryCleanupDate = now
+            }
+        }
     }
     }
 
 
     private func subscribe() {
     private func subscribe() {

+ 5 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -9,6 +9,8 @@ import Swinject
 
 
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
 
+    let coreDataStack = CoreDataStack.shared
+
     // Dependencies Assembler
     // Dependencies Assembler
     // contain all dependencies Assemblies
     // contain all dependencies Assemblies
     // TODO: Remove static key after update "Use Dependencies" logic
     // TODO: Remove static key after update "Use Dependencies" logic
@@ -55,6 +57,9 @@ import Swinject
             "iAPS Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(Bundle.main.buildDate)] [buildExpires: \(Bundle.main.profileExpiration)]"
             "iAPS Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(Bundle.main.buildDate)] [buildExpires: \(Bundle.main.profileExpiration)]"
         )
         )
         loadServices()
         loadServices()
+
+        // Clear the persistentHistory every time the app starts
+        coreDataStack.cleanupPersistentHistory(before: Date.oneWeekAgo)
     }
     }
 
 
     var body: some Scene {
     var body: some Scene {

+ 80 - 45
Model/CoreDataStack.swift

@@ -32,7 +32,15 @@ class CoreDataStack: ObservableObject {
     }
     }
 
 
     /// A persistent history token used for fetching transactions from the store
     /// A persistent history token used for fetching transactions from the store
-    private var lastToken: NSPersistentHistoryToken?
+    /// Save the last token to User defaults
+    private var lastToken: NSPersistentHistoryToken? {
+        get {
+            return UserDefaults.standard.lastHistoryToken
+        }
+        set {
+            UserDefaults.standard.lastHistoryToken = newValue
+        }
+    }
 
 
     /// A persistent container to set up the Core Data Stack
     /// A persistent container to set up the Core Data Stack
     lazy var persistentContainer: NSPersistentContainer = {
     lazy var persistentContainer: NSPersistentContainer = {
@@ -120,7 +128,75 @@ class CoreDataStack: ObservableObject {
         }
         }
     }
     }
 
 
-    // MARK: - Fetch Requests
+    // Clean old Persistent History
+    /// - Tag: clearHistory
+    func cleanupPersistentHistory(before date: Date) {
+        let taskContext = newTaskContext()
+        taskContext.name = "cleanPersistentHistoryContext"
+
+        taskContext.perform {
+            let deleteHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
+            do {
+                try taskContext.execute(deleteHistoryRequest)
+                debugPrint("\(DebuggingIdentifiers.succeeded) Successfully deleted persistent history before \(date)")
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history before \(date): \(error.localizedDescription)"
+                )
+            }
+        }
+    }
+}
+
+// MARK: - Delete
+
+extension CoreDataStack {
+
+    /// Synchronously delete entries with specified object IDs
+    ///  - Tag: synchronousDelete
+    func deleteObject(identifiedBy objectIDs: [NSManagedObjectID]) {
+        let viewContext = persistentContainer.viewContext
+        debugPrint("Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
+
+        viewContext.perform {
+            objectIDs.forEach { objectID in
+                let entryToDelete = viewContext.object(with: objectID)
+                viewContext.delete(entryToDelete)
+            }
+        }
+
+        debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
+    }
+
+    /// Asynchronously deletes records
+    ///  - 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)")
+//    }
+}
+
+// MARK: - Fetch Requests
+
+extension CoreDataStack {
 
 
     // Fetch in background thread
     // Fetch in background thread
     /// - Tag: backgroundFetch
     /// - Tag: backgroundFetch
@@ -175,7 +251,7 @@ class CoreDataStack: ObservableObject {
     }
     }
 
 
     // TODO: -refactor this, currently only the BolusStateModel uses this because we need to fetch in the background, then do calculations and after this update the UI
     // TODO: -refactor this, currently only the BolusStateModel uses this because we need to fetch in the background, then do calculations and after this update the UI
-    
+
     // Fetch and update UI
     // Fetch and update UI
     /// - Tag: uiFetch
     /// - Tag: uiFetch
     func fetchEntitiesAndUpdateUI<T: NSManagedObject>(
     func fetchEntitiesAndUpdateUI<T: NSManagedObject>(
@@ -295,50 +371,9 @@ class CoreDataStack: ObservableObject {
         }
         }
     }
     }
 
 
-    // MARK: - Delete
-
-    /// Synchronously delete entries with specified object IDs
-    ///  - Tag: synchronousDelete
-    func deleteObject(identifiedBy objectIDs: [NSManagedObjectID]) {
-        let viewContext = persistentContainer.viewContext
-        debugPrint("Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
-
-        viewContext.perform {
-            objectIDs.forEach { objectID in
-                let entryToDelete = viewContext.object(with: objectID)
-                viewContext.delete(entryToDelete)
-            }
-        }
-
-        debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
-    }
-
-    /// Asynchronously deletes records
-    ///  - 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)")
-//    }
 }
 }
 
 
+
 // MARK: - Save
 // MARK: - Save
 
 
 extension NSManagedObjectContext {
 extension NSManagedObjectContext {

+ 30 - 0
Model/Helper/UserDefaultsExtension.swift

@@ -0,0 +1,30 @@
+//
+//  UserDefaultsExtension.swift
+//  FreeAPS
+//
+//  Created by Marvin Polscheit on 26.05.24.
+//
+
+import Foundation
+import CoreData
+
+extension UserDefaults {
+    private enum Keys {
+        static let lastHistoryToken = "lastHistoryToken"
+    }
+
+    var lastHistoryToken: NSPersistentHistoryToken? {
+        get {
+            guard let data = data(forKey: Keys.lastHistoryToken) else { return nil }
+            return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: data)
+        }
+        set {
+            guard let token = newValue else {
+                removeObject(forKey: Keys.lastHistoryToken)
+                return
+            }
+            let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
+            set(data, forKey: Keys.lastHistoryToken)
+        }
+    }
+}