Explorar o código

Change Return type of Core Data Observer to Set<NSManagedObjectID>

Improves Thread Safety
polscm32 hai 1 ano
pai
achega
5be6a93ffc

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

@@ -119,7 +119,7 @@ extension Home {
         let batteryFetchContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
-        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
         private var subscriptions = Set<AnyCancellable>()
 
         typealias PumpEvent = PumpEventStored.EventType

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

@@ -121,7 +121,7 @@ extension Treatments {
 
         var isActive: Bool = false
 
-        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
         private var subscriptions = Set<AnyCancellable>()
 
         typealias PumpEvent = PumpEventStored.EventType

+ 1 - 1
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -19,7 +19,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var storage: FileStorage!
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     private var glucoseFormatter: NumberFormatter {

+ 1 - 1
FreeAPS/Sources/Services/ContactImage/ContactImageManager.swift

@@ -32,7 +32,7 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     private var units: GlucoseUnits = .mgdL

+ 1 - 1
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -57,7 +57,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
 
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     var isAvailableOnCurrentDevice: Bool {

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

@@ -47,7 +47,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
     let context = CoreDataStack.shared.newTaskContext()
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
 

+ 22 - 39
FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -79,7 +79,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private var lastEnactedDetermination: Determination?
     private var lastSuggestedDetermination: Determination?
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     init(resolver: Resolver) {
@@ -119,25 +119,18 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     private func registerHandlers() {
         coreDataPublisher?
             .filterByEntityName("OrefDetermination")
-            .sink { [weak self] determinationObjects in
+            .sink { [weak self] objectIDs 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
+                // 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)
+                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
                         let results = try self.backgroundContext.fetch(request)
 
-                        // Safely filter out anything thats deleted or already uploaded
+                        // 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
@@ -174,21 +167,14 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filterByEntityName("PumpEventStored")
-            .sink { [weak self] pumpEventObjects in
+            .sink { [weak self] objectIDs in
                 guard let self = self else { return }
 
-                // 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)
+                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
                         let results = try self.backgroundContext.fetch(request)
 
                         // Safely filter out anything that’s deleted or already uploaded
@@ -199,27 +185,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                             self.uploadPumpHistorySubject.send()
                         }
                     } catch {
-                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                        debugPrint("Failed to fetch PumpEventStored objects: \(error)")
                     }
                 }
-            }.store(in: &subscriptions)
+            }
+            .store(in: &subscriptions)
 
         coreDataPublisher?.filterByEntityName("CarbEntryStored")
-            .sink { [weak self] carbEntryObjects in
+            .sink { [weak self] objectIDs 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)
+                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
                         let results = try self.backgroundContext.fetch(request)
 
                         // Safely filter out anything that’s deleted or already uploaded
@@ -230,17 +210,20 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                             self.uploadCarbsSubject.send()
                         }
                     } catch {
-                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                        debugPrint("Failed to fetch CarbEntryStored objects: \(error)")
                     }
                 }
-            }.store(in: &subscriptions)
+            }
+            .store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadManualGlucose()
+        coreDataPublisher?.filterByEntityName("GlucoseStored")
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task.detached {
+                    await self.uploadManualGlucose()
+                }
             }
-        }.store(in: &subscriptions)
+            .store(in: &subscriptions)
     }
 
     func registerSubscribers() {

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

@@ -40,7 +40,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
     private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     @PersistedProperty(key: "TidepoolState") var rawTidepoolManager: Service.RawValue?

+ 1 - 1
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -65,7 +65,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     let firstInterval = 20 // min

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

@@ -58,7 +58,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     let context = CoreDataStack.shared.newTaskContext()
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
     private var lifetime = Lifetime()

+ 19 - 9
Model/CoreDataObserver.swift

@@ -2,24 +2,34 @@ import Combine
 import CoreData
 import Foundation
 
-func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObject>, Never> {
+func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObjectID>, Never> {
     Foundation.NotificationCenter.default
         .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
         .map { notification in
-            guard let userInfo = notification.userInfo else { return Set<NSManagedObject>() }
+            guard let userInfo = notification.userInfo else { return Set<NSManagedObjectID>() }
 
-            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            var objectIDs = Set<NSManagedObjectID>()
 
-            return objects
+            if let inserted = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(inserted.map(\.objectID))
+            }
+            if let updated = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(updated.map(\.objectID))
+            }
+            if let deleted = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(deleted.map(\.objectID))
+            }
+
+            return objectIDs
         }
 }
 
-extension Publisher where Output == Set<NSManagedObject> {
+extension Publisher where Output == Set<NSManagedObjectID> {
     func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
-        filter { objects in
-            objects.contains(where: { $0.entity.name == name })
+        filter { objectIDs in
+            objectIDs.contains { objectID in
+                objectID.entity.name == name
+            }
         }
     }
 }