Просмотр исходного кода

Add debounce and dedup handling to NS Manager and LA Bridge

Deniz Cengiz 1 год назад
Родитель
Сommit
1eff433a9a

+ 6 - 12
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -282,8 +282,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         return await coredataContext.perform {
         return await coredataContext.perform {
@@ -314,8 +313,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -369,8 +367,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -400,8 +397,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -431,8 +427,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
@@ -463,8 +458,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }
         guard let fetchedResults = results as? [GlucoseStored] else { return [] }

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

@@ -290,8 +290,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
             key: "timestamp",
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         return await context.perform { [self] in
         return await context.perform { [self] in
@@ -450,8 +449,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
             key: "timestamp",
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
@@ -493,8 +491,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             key: "timestamp",
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
         guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }

+ 1 - 2
FreeAPS/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -697,8 +697,7 @@ extension Treatments.StateModel {
             onContext: glucoseFetchContext,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
         )
 
 
         return await glucoseFetchContext.perform {
         return await glucoseFetchContext.perform {

+ 10 - 1
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -49,6 +49,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
 
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
+    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         coreDataPublisher =
         coreDataPublisher =
@@ -103,7 +104,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
 
         coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
-            self.cobOrIobDidUpdate()
+            self.orefDeterminationSubject.send()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
     }
     }
 
 
@@ -115,6 +116,14 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
+
+        orefDeterminationSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                self.cobOrIobDidUpdate()
+            }
+            .store(in: &subscriptions)
     }
     }
 
 
     private func cobOrIobDidUpdate() {
     private func cobOrIobDidUpdate() {

+ 1 - 1
FreeAPS/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -363,7 +363,7 @@ extension NightscoutAPI {
 //        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
 //        debugPrint("Upload successful, response data: \(String(data: data, encoding: .utf8) ?? "No data")")
     }
     }
 
 
-    func uploadStatus(_ status: NightscoutStatus) async throws {
+    func uploadDeviceStatus(_ status: NightscoutStatus) async throws {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme
         components.host = url.host
         components.host = url.host

+ 156 - 58
FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -12,7 +12,7 @@ protocol NightscoutManager: GlucoseSource {
     func deleteCarbs(withID id: String) async
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteManualGlucose(withID id: String) async
     func deleteManualGlucose(withID id: String) async
-    func uploadStatus() async
+    func uploadDeviceStatus() async
     func uploadGlucose() async
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadPumpHistory() async
@@ -39,6 +39,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() private var reachabilityManager: ReachabilityManager!
     @Injected() var healthkitManager: HealthKitManager!
     @Injected() var healthkitManager: HealthKitManager!
 
 
+    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
     private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
     private let uploadOverridesSubject = PassthroughSubject<Void, Never>()
     private let uploadPumpHistorySubject = PassthroughSubject<Void, Never>()
     private let uploadPumpHistorySubject = PassthroughSubject<Void, Never>()
     private let uploadCarbsSubject = PassthroughSubject<Void, Never>()
     private let uploadCarbsSubject = PassthroughSubject<Void, Never>()
@@ -91,52 +92,13 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 
-        glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadGlucose()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadOverridesSubject
-            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadOverrides()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadPumpHistorySubject
-            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadPumpHistory()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadCarbsSubject
-            .debounce(for: .seconds(1), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadCarbs()
-                }
-            }
-            .store(in: &subscriptions)
-
+        registerSubscribers()
         registerHandlers()
         registerHandlers()
         setupNotification()
         setupNotification()
 
 
         /// Ensure that Nightscout Manager holds the `lastEnactedDetermination`, if one exists, on initialization.
         /// Ensure that Nightscout Manager holds the `lastEnactedDetermination`, if one exists, on initialization.
         /// We have to set this here in `init()`, so there's a `lastEnactedDetermination` available after an app restart
         /// We have to set this here in `init()`, so there's a `lastEnactedDetermination` available after an app restart
-        /// for `uploadStatus()`, as within that fuction `lastEnactedDetermination` is reassigned at the very end of the function.
+        /// for `uploadDeviceStatus()`, as within that fuction `lastEnactedDetermination` is reassigned at the very end of the function.
         /// This way, we ensure the latest enacted determination is always part of `devicestatus` and avoid having instances
         /// This way, we ensure the latest enacted determination is always part of `devicestatus` and avoid having instances
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         Task {
         Task {
@@ -155,12 +117,39 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadStatus()
+        coreDataPublisher?
+            .filterByEntityName("OrefDetermination")
+            .sink { [weak self] determinationObjects in
+                guard let self = self else { return }
+
+                // Collect objectIDs of determinationObjects here
+                let determinationObjectsIDs = determinationObjects
+                    .compactMap { $0 as? OrefDetermination }
+                    .map(\.objectID)
+
+                guard !determinationObjectsIDs.isEmpty else { return }
+
+                // Now hop onto the background context’s queue
+                self.backgroundContext.perform {
+                    do {
+                        // Fetch only those determination objects
+                        let request: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
+                        request.predicate = NSPredicate(format: "SELF IN %@", determinationObjectsIDs)
+                        let results = try self.backgroundContext.fetch(request)
+
+                        // Safely filter out anything that’s deleted or already uploaded
+                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
+
+                        // If valid, proceed to send to subject for further processing
+                        if !unuploaded.isEmpty {
+                            self.orefDeterminationSubject.send()
+                        }
+                    } catch {
+                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                    }
+                }
             }
             }
-        }.store(in: &subscriptions)
+            .store(in: &subscriptions)
 
 
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             self?.uploadOverridesSubject.send()
             self?.uploadOverridesSubject.send()
@@ -184,13 +173,67 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
-            self?.uploadPumpHistorySubject.send()
-        }.store(in: &subscriptions)
+        coreDataPublisher?.filterByEntityName("PumpEventStored")
+            .sink { [weak self] pumpEventObjects in
+                guard let self = self else { return }
 
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
-            self?.uploadCarbsSubject.send()
-        }.store(in: &subscriptions)
+                // Collect objectIDs of pumpEventObjects here
+                let pumpEventObjectsIDs = pumpEventObjects
+                    .compactMap { $0 as? PumpEventStored }
+                    .map(\.objectID)
+
+                guard !pumpEventObjectsIDs.isEmpty else { return }
+
+                // Now hop onto the background context’s queue
+                self.backgroundContext.perform {
+                    do {
+                        let request: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
+                        request.predicate = NSPredicate(format: "SELF IN %@", pumpEventObjectsIDs)
+                        let results = try self.backgroundContext.fetch(request)
+
+                        // Safely filter out anything that’s deleted or already uploaded
+                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
+
+                        // If valid, proceed to send to subject for further processing
+                        if !unuploaded.isEmpty {
+                            self.uploadPumpHistorySubject.send()
+                        }
+                    } catch {
+                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                    }
+                }
+            }.store(in: &subscriptions)
+
+        coreDataPublisher?.filterByEntityName("CarbEntryStored")
+            .sink { [weak self] carbEntryObjects in
+                guard let self = self else { return }
+
+                // Collect objectIDs of carbEntryObjects here
+                let carbEntryObjecIDs = carbEntryObjects
+                    .compactMap { $0 as? CarbEntryStored }
+                    .map(\.objectID)
+
+                guard !carbEntryObjecIDs.isEmpty else { return }
+
+                // Now hop onto the background context’s queue
+                self.backgroundContext.perform {
+                    do {
+                        let request: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
+                        request.predicate = NSPredicate(format: "SELF IN %@", carbEntryObjecIDs)
+                        let results = try self.backgroundContext.fetch(request)
+
+                        // Safely filter out anything that’s deleted or already uploaded
+                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
+
+                        // If valid, proceed to send to subject for further processing
+                        if !unuploaded.isEmpty {
+                            self.uploadCarbsSubject.send()
+                        }
+                    } catch {
+                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                    }
+                }
+            }.store(in: &subscriptions)
 
 
         coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
         coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
@@ -200,6 +243,61 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
     }
     }
 
 
+    func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadGlucose()
+                }
+            }
+            .store(in: &subscriptions)
+
+        /// We add debouncing behavior here for two main reasons
+        /// 1. To ensure that any upload flag updates have properly been performed, and in subsequent fetching processes only truly unuploaded data is fetched
+        /// 2. To not spam the user's NS site with a high number of uploads in a very short amount of time (less than 1sec)
+        orefDeterminationSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadDeviceStatus()
+                }
+            }
+            .store(in: &subscriptions)
+
+        uploadOverridesSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadOverrides()
+                }
+            }
+            .store(in: &subscriptions)
+
+        uploadPumpHistorySubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadPumpHistory()
+                }
+            }
+            .store(in: &subscriptions)
+
+        uploadCarbsSubject
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] in
+                guard let self = self else { return }
+                Task {
+                    await self.uploadCarbs()
+                }
+            }
+            .store(in: &subscriptions)
+    }
+
     func setupNotification() {
     func setupNotification() {
         Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
         Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
             .sink { [weak self] _ in
             .sink { [weak self] _ in
@@ -422,7 +520,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     ///
     ///
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Returns: Nothing.
     /// - Returns: Nothing.
-    func uploadStatus() async {
+    func uploadDeviceStatus() async {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             return
             return
@@ -531,15 +629,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         )
         )
 
 
         do {
         do {
-            try await nightscout.uploadStatus(status)
-            debug(.nightscout, "Status uploaded")
+            try await nightscout.uploadDeviceStatus(status)
+            debug(.nightscout, "NSDeviceStatus with Determination uploaded")
 
 
             if let enacted = fetchedEnactedDetermination {
             if let enacted = fetchedEnactedDetermination {
                 await updateOrefDeterminationAsUploaded([enacted])
                 await updateOrefDeterminationAsUploaded([enacted])
+                debug(.nightscout, "Flagged last fetched enacted determination as uploaded")
             }
             }
 
 
             if let suggested = fetchedSuggestedDetermination {
             if let suggested = fetchedSuggestedDetermination {
                 await updateOrefDeterminationAsUploaded([suggested])
                 await updateOrefDeterminationAsUploaded([suggested])
+                debug(.nightscout, "Flagged last fetched suggested determination as uploaded")
             }
             }
 
 
             if let lastEnactedDetermination = fetchedEnactedDetermination {
             if let lastEnactedDetermination = fetchedEnactedDetermination {
@@ -549,8 +649,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             if let lastSuggestedDetermination = fetchedSuggestedDetermination {
             if let lastSuggestedDetermination = fetchedSuggestedDetermination {
                 self.lastSuggestedDetermination = lastSuggestedDetermination
                 self.lastSuggestedDetermination = lastSuggestedDetermination
             }
             }
-
-            debug(.nightscout, "NSDeviceStatus with Determination uploaded")
         } catch {
         } catch {
             debug(.nightscout, error.localizedDescription)
             debug(.nightscout, error.localizedDescription)
         }
         }

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

@@ -35,7 +35,7 @@ extension NSPredicate {
     static var enactedDeterminationsNotYetUploadedToNightscout: NSPredicate {
     static var enactedDeterminationsNotYetUploadedToNightscout: NSPredicate {
         NSPredicate(
         NSPredicate(
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND enacted == %@",
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND enacted == %@",
-            Date.sixHoursAgo as NSDate,
+            Date.oneDayAgo as NSDate,
             false as NSNumber,
             false as NSNumber,
             true as NSNumber
             true as NSNumber
         )
         )
@@ -44,7 +44,7 @@ extension NSPredicate {
     static var suggestedDeterminationsNotYetUploadedToNightscout: NSPredicate {
     static var suggestedDeterminationsNotYetUploadedToNightscout: NSPredicate {
         NSPredicate(
         NSPredicate(
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND (enacted == %@ OR enacted == nil OR enacted != %@)",
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND (enacted == %@ OR enacted == nil OR enacted != %@)",
-            Date.sixHoursAgo as NSDate,
+            Date.oneDayAgo as NSDate,
             false as NSNumber,
             false as NSNumber,
             true as NSNumber,
             true as NSNumber,
             true as NSNumber
             true as NSNumber