Преглед изворни кода

Adapt the remaining classes to the new pattern

Marvin Polscheit пре 1 месец
родитељ
комит
8523d8f76c
26 измењених фајлова са 443 додато и 312 уклоњено
  1. 52 31
      Trio/Sources/APS/APSManager.swift
  2. 13 10
      Trio/Sources/APS/DeviceDataManager.swift
  3. 6 4
      Trio/Sources/APS/FetchGlucoseManager.swift
  4. 0 1
      Trio/Sources/APS/FetchTreatmentsManager.swift
  5. 56 36
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  6. 19 12
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  7. 17 9
      Trio/Sources/APS/Storage/TDDStorage.swift
  8. 0 2
      Trio/Sources/Modules/History/HistoryStateModel.swift
  9. 8 5
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  10. 8 5
      Trio/Sources/Services/Calendar/CalendarManager.swift
  11. 8 5
      Trio/Sources/Services/ContactImage/ContactImageManager.swift
  12. 22 16
      Trio/Sources/Services/HealthKit/HealthKitManager.swift
  13. 2 1
      Trio/Sources/Services/IOB/IOBService.swift
  14. 7 0
      Trio/Sources/Services/LiveActivity/Data/DataManager.swift
  15. 0 3
      Trio/Sources/Services/LiveActivity/LiveActivityManager.swift
  16. 62 43
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  17. 90 86
      Trio/Sources/Services/Network/TidepoolManager.swift
  18. 3 1
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift
  19. 0 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift
  20. 4 3
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  21. 19 11
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  22. 14 10
      Trio/Sources/Services/WatchManager/GarminManager.swift
  23. 0 1
      Trio/Sources/Shortcuts/BaseIntentsRequest.swift
  24. 15 6
      Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  25. 0 2
      Trio/Sources/Shortcuts/State/StateIntentRequest.swift
  26. 18 7
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

+ 52 - 31
Trio/Sources/APS/APSManager.swift

@@ -89,9 +89,6 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    let privateContext = CoreDataStack.shared.newTaskContext()
-
     private var openAPS: OpenAPS!
 
     private var lifetime = Lifetime()
@@ -332,13 +329,15 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func calculateLoopInterval() async -> Double? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "calculateLoopInterval"
         do {
-            return try await privateContext.perform {
+            return try await context.perform {
                 let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
                 let sortStats = NSSortDescriptor(key: "end", ascending: false)
                 requestStats.sortDescriptors = [sortStats]
                 requestStats.fetchLimit = 1
-                let previousLoop = try self.privateContext.fetch(requestStats)
+                let previousLoop = try context.fetch(requestStats)
 
                 if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
                     return self.roundDouble(
@@ -449,7 +448,9 @@ final class BaseAPSManager: APSManager, Injectable {
         var invalidGlucoseError: String?
 
         // Perform the context-related checks and actions
-        let isValidGlucoseData = await privateContext.perform { [weak self] in
+        let validationContext = CoreDataStack.shared.newTaskContext()
+        validationContext.name = "determineBasal.validation"
+        let isValidGlucoseData = await validationContext.perform { [weak self] in
             guard let self else { return false }
 
             guard glucose.count > 2 else {
@@ -658,16 +659,18 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func fetchCurrentTempBasal(date: Date) async throws -> TempBasal {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchCurrentTempBasal"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
-            onContext: privateContext,
+            onContext: context,
             predicate: NSPredicate.recentPumpHistory,
             key: "timestamp",
             ascending: false,
             fetchLimit: 1
         )
 
-        let fetchedTempBasal = await privateContext.perform {
+        let fetchedTempBasal = await context.perform {
             guard let fetchedResults = results as? [PumpEventStored],
                   let tempBasalEvent = fetchedResults.first,
                   let tempBasal = tempBasalEvent.tempBasal,
@@ -733,9 +736,11 @@ final class BaseAPSManager: APSManager, Injectable {
     private func setValues(determinationID: NSManagedObjectID) async throws
         -> (NSDecimalNumber?, TimeInterval?, NSDecimalNumber?)
     {
-        return try await privateContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "setValues"
+        return try await context.perform {
             do {
-                let determination = try self.privateContext.existingObject(with: determinationID) as? OrefDetermination
+                let determination = try context.existingObject(with: determinationID) as? OrefDetermination
 
                 let rate = determination?.rate
                 let duration = determination?.duration.flatMap { TimeInterval(truncating: $0) * 60 }
@@ -766,8 +771,10 @@ final class BaseAPSManager: APSManager, Injectable {
                 return
             }
 
-            try await privateContext.perform {
-                guard let determinationUpdated = try self.privateContext
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "reportEnacted"
+            try await context.perform {
+                guard let determinationUpdated = try context
                     .existingObject(with: determinationID) as? OrefDetermination
                 else {
                     debug(.apsManager, "Could not find determination object in context")
@@ -778,8 +785,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 determinationUpdated.enacted = wasEnacted
                 determinationUpdated.isUploadedToNS = false
 
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
             }
         } catch {
@@ -827,7 +834,9 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
-        privateContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "tir"
+        return context.perform {
             let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
             let totalReadings = justGlucoseArray.count
             let highLimit = settingsManager.settings.high
@@ -947,9 +956,11 @@ final class BaseAPSManager: APSManager, Injectable {
 
     // fetch glucose for time interval
     func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async throws -> [GlucoseStored] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: privateContext,
+            onContext: context,
             predicate: predicate,
             key: "date",
             ascending: false,
@@ -957,7 +968,7 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
         )
 
-        return try await privateContext.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -972,9 +983,11 @@ final class BaseAPSManager: APSManager, Injectable {
         requestStats.sortDescriptors = [sortStats]
         requestStats.fetchLimit = 1
 
-        return await privateContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "lastLoopForStats"
+        return await context.perform {
             do {
-                return try self.privateContext.fetch(requestStats).first?.lastrun
+                return try context.fetch(requestStats).first?.lastrun
             } catch {
                 print(error.localizedDescription)
                 return .distantPast
@@ -991,9 +1004,11 @@ final class BaseAPSManager: APSManager, Injectable {
         let sortLSR = NSSortDescriptor(key: "start", ascending: false)
         requestLSR.sortDescriptors = [sortLSR]
 
-        return await privateContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "loopStats"
+        return await context.perform {
             do {
-                let lsr = try self.privateContext.fetch(requestLSR)
+                let lsr = try context.fetch(requestLSR)
 
                 // Compute LoopStats for 24 hours
                 let oneDayLoops = self.loops(lsr)
@@ -1062,7 +1077,9 @@ final class BaseAPSManager: APSManager, Injectable {
                 batchSize: 1000
             )
 
-            return await privateContext.perform {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "glucoseForStats"
+            return await context.perform {
                 let units = self.settingsManager.settings.units
 
                 // First date
@@ -1184,8 +1201,10 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func loopStats(loopStatRecord: LoopStats) {
-        privateContext.perform {
-            let nLS = LoopStatRecord(context: self.privateContext)
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "loopStats.save"
+        context.perform {
+            let nLS = LoopStatRecord(context: context)
             nLS.start = loopStatRecord.start
             nLS.end = loopStatRecord.end ?? Date()
             nLS.loopStatus = loopStatRecord.loopStatus
@@ -1193,8 +1212,8 @@ final class BaseAPSManager: APSManager, Injectable {
             nLS.interval = loopStatRecord.interval ?? 0.0
 
             do {
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch {
                 print(error.localizedDescription)
             }
@@ -1295,7 +1314,9 @@ extension BaseAPSManager: PumpManagerStatusObserver {
     func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
         let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
 
-        privateContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "storeBatteryStatus"
+        context.perform {
             /// only update the last item with the current battery infos instead of saving a new one each time
             let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
             fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
@@ -1303,13 +1324,13 @@ extension BaseAPSManager: PumpManagerStatusObserver {
             fetchRequest.fetchLimit = 1
 
             do {
-                let results = try self.privateContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 let batteryToStore: OpenAPS_Battery
 
                 if let existingBattery = results.first {
                     batteryToStore = existingBattery
                 } else {
-                    batteryToStore = OpenAPS_Battery(context: self.privateContext)
+                    batteryToStore = OpenAPS_Battery(context: context)
                     batteryToStore.id = UUID()
                 }
 
@@ -1319,8 +1340,8 @@ extension BaseAPSManager: PumpManagerStatusObserver {
                 batteryToStore.status = percent > 10 ? "normal" : "low"
                 batteryToStore.display = status.pumpBatteryChargeRemaining != nil
 
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch {
                 debug(.apsManager, "Failed to fetch or save battery: \(error)")
             }

+ 13 - 10
Trio/Sources/APS/DeviceDataManager.swift

@@ -81,7 +81,6 @@ 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.newTaskContext()
 
     var pumpManager: PumpManagerUI? {
         didSet {
@@ -159,8 +158,10 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         display: simulatorPump.state.pumpBatteryChargeRemaining != nil
                     )
                     Task {
-                        await self.privateContext.perform {
-                            let saveBatteryToCoreData = OpenAPS_Battery(context: self.privateContext)
+                        let context = CoreDataStack.shared.newTaskContext()
+                        context.name = "storeSimulatorBattery"
+                        await context.perform {
+                            let saveBatteryToCoreData = OpenAPS_Battery(context: context)
                             saveBatteryToCoreData.id = UUID()
                             saveBatteryToCoreData.date = Date()
                             saveBatteryToCoreData.percent = Double(batteryPercent)
@@ -170,8 +171,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                             saveBatteryToCoreData.display = simulatorPump.state.pumpBatteryChargeRemaining != nil
 
                             do {
-                                guard self.privateContext.hasChanges else { return }
-                                try self.privateContext.save()
+                                guard context.hasChanges else { return }
+                                try context.save()
                             } catch {
                                 print(error.localizedDescription)
                             }
@@ -194,18 +195,20 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
                 // Remove OpenAPS_Battery entries
                 Task {
-                    await self.privateContext.perform {
+                    let context = CoreDataStack.shared.newTaskContext()
+                    context.name = "deleteBatteryEntries"
+                    await context.perform {
                         let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
 
                         do {
-                            let batteryEntries = try self.privateContext.fetch(fetchRequest)
+                            let batteryEntries = try context.fetch(fetchRequest)
 
                             for entry in batteryEntries {
-                                self.privateContext.delete(entry)
+                                context.delete(entry)
                             }
 
-                            guard self.privateContext.hasChanges else { return }
-                            try self.privateContext.save()
+                            guard context.hasChanges else { return }
+                            try context.save()
 
                         } catch {
                             debug(.deviceManager, "Failed to delete OpenAPS_Battery entries: \(error)")

+ 6 - 4
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -56,8 +56,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
     private lazy var simulatorSource = GlucoseSimulatorSource()
 
-    private let context = CoreDataStack.shared.newTaskContext()
-
     /// Enforce mutual exclusion on calls to glucoseStoreAndHeartDecision
     private let glucoseStoreAndHeartLock = DispatchSemaphore(value: 1)
 
@@ -278,7 +276,9 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         try await glucoseStorage.storeGlucose(filtered)
 
         if settingsManager.settings.smoothGlucose {
-            await exponentialSmoothingGlucose(context: context)
+            let smoothingContext = CoreDataStack.shared.newTaskContext()
+            smoothingContext.name = "exponentialSmoothingGlucose"
+            await exponentialSmoothingGlucose(context: smoothingContext)
         }
 
         deviceDataManager.heartbeat(date: Date())
@@ -355,7 +355,9 @@ extension BaseFetchGlucoseManager: SettingsObserver {
 
             self.glucoseStoreAndHeartLock.wait()
             Task {
-                await self.exponentialSmoothingGlucose(context: self.context)
+                let context = CoreDataStack.shared.newTaskContext()
+                context.name = "exponentialSmoothingGlucose"
+                await self.exponentialSmoothingGlucose(context: context)
                 self.glucoseStoreAndHeartLock.signal()
             }
         }

+ 0 - 1
Trio/Sources/APS/FetchTreatmentsManager.swift

@@ -14,7 +14,6 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
-    private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
     init(resolver: Resolver) {
         injectServices(resolver)

+ 56 - 36
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -10,10 +10,14 @@ final class OpenAPS {
     private let storage: FileStorage
     private let tddStorage: TDDStorage
 
-    let context = CoreDataStack.shared.newTaskContext()
-
     let jsonConverter = JSONConverter()
 
+    private func newContext(_ name: String) -> NSManagedObjectContext {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = name
+        return context
+    }
+
     init(storage: FileStorage, tddStorage: TDDStorage) {
         self.storage = storage
         self.tddStorage = tddStorage
@@ -32,9 +36,9 @@ final class OpenAPS {
     }
 
     // Use the helper function for cleaner code
-    func processDetermination(_ determination: Determination) async {
+    func processDetermination(_ determination: Determination, on context: NSManagedObjectContext) async {
         await context.perform {
-            let newOrefDetermination = OrefDetermination(context: self.context)
+            let newOrefDetermination = OrefDetermination(context: context)
             newOrefDetermination.id = UUID()
             newOrefDetermination.insulinSensitivity = self.decimalToNSDecimalNumber(determination.isf)
             newOrefDetermination.currentTarget = self.decimalToNSDecimalNumber(determination.current_target)
@@ -64,14 +68,14 @@ final class OpenAPS {
                 ["iob": predictions.iob, "zt": predictions.zt, "cob": predictions.cob, "uam": predictions.uam]
                     .forEach { type, values in
                         if let values = values {
-                            let forecast = Forecast(context: self.context)
+                            let forecast = Forecast(context: context)
                             forecast.id = UUID()
                             forecast.type = type
                             forecast.date = Date()
                             forecast.orefDetermination = newOrefDetermination
 
                             for (index, value) in values.enumerated() {
-                                let forecastValue = ForecastValue(context: self.context)
+                                let forecastValue = ForecastValue(context: context)
                                 forecastValue.index = Int32(index)
                                 forecastValue.value = Int32(value)
                                 forecast.addToForecastValues(forecastValue)
@@ -83,14 +87,14 @@ final class OpenAPS {
         }
 
         // First save the current Determination to Core Data
-        await attemptToSaveContext()
+        await attemptToSaveContext(on: context)
     }
 
-    func attemptToSaveContext() async {
+    func attemptToSaveContext(on context: NSManagedObjectContext) async {
         await context.perform {
             do {
-                guard self.context.hasChanges else { return }
-                try self.context.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch {
                 debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Determination to Core Data")
             }
@@ -156,7 +160,11 @@ final class OpenAPS {
         return jsonConverter.convertToJSON(algorithmGlucose)
     }
 
-    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
+    private func fetchAndProcessCarbs(
+        on context: NSManagedObjectContext,
+        additionalCarbs: Decimal? = nil,
+        carbsDate: Date? = nil
+    ) async throws -> String {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: context,
@@ -208,7 +216,7 @@ final class OpenAPS {
         return json
     }
 
-    private func fetchPumpHistoryObjectIDs() async throws -> [NSManagedObjectID]? {
+    private func fetchPumpHistoryObjectIDs(on context: NSManagedObjectContext) async throws -> [NSManagedObjectID]? {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
@@ -228,6 +236,7 @@ final class OpenAPS {
     }
 
     private func parsePumpHistory(
+        on context: NSManagedObjectContext,
         _ pumpHistoryObjectIDs: [NSManagedObjectID],
         simulatedBolusAmount: Decimal? = nil
     ) async throws -> String {
@@ -240,12 +249,12 @@ final class OpenAPS {
         // the oldest event in pump history can be a resume with no preceding pump
         // activity. oref interprets this as the end of a suspend that never started,
         // which drives negative IOB and can cause excessive insulin delivery.
-        let orphanedResumes = try await fetchOrphanedResumes()
+        let orphanedResumes = try await fetchOrphanedResumes(on: context)
 
         // Execute all operations on the background context
         return await context.perform {
             // Load and map pump events to DTOs
-            var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes)
+            var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes, on: context)
 
             // Optionally add the IOB as a DTO
             if let simulatedBolusAmount = simulatedBolusAmount {
@@ -260,7 +269,8 @@ final class OpenAPS {
 
     private func loadAndMapPumpEvents(
         _ pumpHistoryObjectIDs: [NSManagedObjectID],
-        orphanedResumes: [NSManagedObjectID]
+        orphanedResumes: [NSManagedObjectID],
+        on context: NSManagedObjectContext
     ) -> [PumpEventDTO] {
         OpenAPS.loadAndMapPumpEvents(pumpHistoryObjectIDs, orphanedResumes: orphanedResumes, from: context)
     }
@@ -328,7 +338,7 @@ final class OpenAPS {
     }
 
     /// Detects a cold-start orphaned resume: returns the resume's object ID if it's an orphaned resume
-    private func fetchOrphanedResumes() async throws -> [NSManagedObjectID] {
+    private func fetchOrphanedResumes(on context: NSManagedObjectContext) async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: context,
@@ -388,14 +398,20 @@ final class OpenAPS {
     ) async throws -> Determination? {
         debug(.openAPS, "Start determineBasal")
 
+        let context = newContext("determineBasal")
+
         // temp_basal
         let tempBasal = currentTemp.rawJSON
 
         // Perform asynchronous calls in parallel
-        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
+        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs(on: context) ?? []
+        async let carbs = fetchAndProcessCarbs(
+            on: context,
+            additionalCarbs: simulatedCarbsAmount ?? 0,
+            carbsDate: simulatedCarbsDate
+        )
         async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: 72)
-        async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
+        async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables(on: context)
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
@@ -415,7 +431,7 @@ final class OpenAPS {
             reservoir,
             hasSufficientTdd
         ) = await (
-            try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
+            try parsePumpHistory(on: context, await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
             try carbs,
             try glucose,
             try prepareTrioCustomOrefVariables,
@@ -482,7 +498,7 @@ final class OpenAPS {
 
             if !simulation {
                 // save to core data asynchronously
-                await processDetermination(determination)
+                await processDetermination(determination, on: context)
             }
 
             return determination
@@ -495,7 +511,7 @@ final class OpenAPS {
         }
     }
 
-    func prepareTrioCustomOrefVariables() async throws -> RawJSON {
+    func prepareTrioCustomOrefVariables(on context: NSManagedObjectContext) async throws -> RawJSON {
         try await context.perform {
             // Retrieve user preferences
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
@@ -506,10 +522,10 @@ final class OpenAPS {
             // Fetch historical events for Total Daily Dose (TDD) calculation
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
-            let historicalTDDData = try self.fetchHistoricalTDDData(from: tenDaysAgo)
+            let historicalTDDData = try self.fetchHistoricalTDDData(from: tenDaysAgo, on: context)
 
             // Fetch the last active Override
-            let activeOverrides = try self.fetchActiveOverrides()
+            let activeOverrides = try self.fetchActiveOverrides(on: context)
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
@@ -531,7 +547,7 @@ final class OpenAPS {
             let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
             let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
 
-            let glucose = try self.fetchGlucose()
+            let glucose = try self.fetchGlucose(on: context)
 
             // Prepare Trio's custom oref variables
             let trioCustomOrefVariablesData = TrioCustomOrefVariables(
@@ -566,9 +582,11 @@ final class OpenAPS {
     func autosense(shouldSmoothGlucose: Bool) async throws -> Autosens? {
         debug(.openAPS, "Start autosens")
 
+        let context = newContext("autosense")
+
         // Perform asynchronous calls in parallel
-        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs()
+        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs(on: context) ?? []
+        async let carbs = fetchAndProcessCarbs(on: context)
         async let glucose = fetchAndProcessGlucose(context: context, shouldSmoothGlucose: shouldSmoothGlucose, fetchLimit: nil)
         async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
         async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
@@ -576,7 +594,7 @@ final class OpenAPS {
 
         // Await the results of asynchronous tasks
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
-            try parsePumpHistory(await pumpHistoryObjectIDs),
+            try parsePumpHistory(on: context, await pumpHistoryObjectIDs),
             try carbs,
             try glucose,
             getProfile,
@@ -635,9 +653,10 @@ final class OpenAPS {
         var adjustedPreferences = preferences
 
         // Check for active Temp Targets and adjust HBT if necessary
+        let context = newContext("createProfiles")
         try await context.perform {
             // Check if a Temp Target is active and check HBT differs from setting and adjust
-            if let activeTempTarget = try self.fetchActiveTempTargets().first,
+            if let activeTempTarget = try self.fetchActiveTempTargets(on: context).first,
                activeTempTarget.enabled,
                let targetValue = activeTempTarget.target?.decimalValue
             {
@@ -924,15 +943,16 @@ final class OpenAPS {
 
     func processAndSave(forecastData: [String: [Int]]) {
         let currentDate = Date()
+        let context = newContext("processAndSave")
 
         context.perform {
             for (type, values) in forecastData {
-                self.createForecast(type: type, values: values, date: currentDate, context: self.context)
+                self.createForecast(type: type, values: values, date: currentDate, context: context)
             }
 
             do {
-                guard self.context.hasChanges else { return }
-                try self.context.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch {
                 print(error.localizedDescription)
             }
@@ -956,7 +976,7 @@ final class OpenAPS {
 
 // Non-Async fetch methods for trio_custom_oref_variables
 extension OpenAPS {
-    func fetchActiveTempTargets() throws -> [TempTargetStored] {
+    func fetchActiveTempTargets(on context: NSManagedObjectContext) throws -> [TempTargetStored] {
         try CoreDataStack.shared.fetchEntities(
             ofType: TempTargetStored.self,
             onContext: context,
@@ -967,7 +987,7 @@ extension OpenAPS {
         ) as? [TempTargetStored] ?? []
     }
 
-    func fetchActiveOverrides() throws -> [OverrideStored] {
+    func fetchActiveOverrides(on context: NSManagedObjectContext) throws -> [OverrideStored] {
         try CoreDataStack.shared.fetchEntities(
             ofType: OverrideStored.self,
             onContext: context,
@@ -978,7 +998,7 @@ extension OpenAPS {
         ) as? [OverrideStored] ?? []
     }
 
-    func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
+    func fetchHistoricalTDDData(from date: Date, on context: NSManagedObjectContext) throws -> [[String: Any]] {
         try CoreDataStack.shared.fetchEntities(
             ofType: TDDStored.self,
             onContext: context,
@@ -989,7 +1009,7 @@ extension OpenAPS {
         ) as? [[String: Any]] ?? []
     }
 
-    func fetchGlucose() throws -> [GlucoseStored] {
+    func fetchGlucose(on context: NSManagedObjectContext) throws -> [GlucoseStored] {
         let results = try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             onContext: context,

+ 19 - 12
Trio/Sources/APS/Storage/ContactImageStorage.swift

@@ -13,9 +13,10 @@ protocol ContactImageStorage {
 final class BaseContactImageStorage: ContactImageStorage, Injectable {
     @Injected() private var settingsManager: SettingsManager!
 
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let makeContext: () -> NSManagedObjectContext
 
-    init(resolver: Resolver) {
+    init(resolver: Resolver, contextProvider: (() -> NSManagedObjectContext)? = nil) {
+        makeContext = contextProvider ?? { CoreDataStack.shared.newTaskContext() }
         injectServices(resolver)
     }
 
@@ -26,16 +27,18 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
     ///
     /// - Returns: An array of `ContactImageEntry` objects.
     func fetchContactImageEntries() async -> [ContactImageEntry] {
+        let context = makeContext()
+        context.name = "fetchContactImageEntries"
         do {
             let results = try await CoreDataStack.shared.fetchEntitiesAsync(
                 ofType: ContactImageEntryStored.self,
-                onContext: backgroundContext,
+                onContext: context,
                 predicate: NSPredicate.all,
                 key: "hasHighContrast",
                 ascending: false
             )
 
-            return try await backgroundContext.perform {
+            return try await context.perform {
                 guard let fetchedContactImageEntries = results as? [ContactImageEntryStored]
                 else { throw CoreDataError.fetchError(function: #function, file: #file)
                 }
@@ -75,8 +78,10 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
     ///
     /// - Parameter contactImageEntry: The `ContactImageEntry` object to be stored.
     func storeContactImageEntry(_ contactImageEntry: ContactImageEntry) async {
-        await backgroundContext.perform {
-            let newContactImageEntry = ContactImageEntryStored(context: self.backgroundContext)
+        let context = makeContext()
+        context.name = "storeContactImageEntry"
+        await context.perform {
+            let newContactImageEntry = ContactImageEntryStored(context: context)
 
             newContactImageEntry.id = UUID()
             newContactImageEntry.name = contactImageEntry.name
@@ -96,8 +101,8 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
             newContactImageEntry.fontWeight = contactImageEntry.fontWeight.asString
 
             do {
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Contact Trick Entry to Core Data with error: \(error.userInfo)"
@@ -114,12 +119,14 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
     ///
     /// - Parameter contactImageEntry: The `ContactImageEntry` object with updated values.
     func updateContactImageEntry(_ contactImageEntry: ContactImageEntry) async {
-        await backgroundContext.perform {
+        let context = makeContext()
+        context.name = "updateContactImageEntry"
+        await context.perform {
             let fetchRequest: NSFetchRequest<ContactImageEntryStored> = ContactImageEntryStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "contactId == %@", contactImageEntry.contactId ?? "")
 
             do {
-                if let existingEntry = try self.backgroundContext.fetch(fetchRequest).first {
+                if let existingEntry = try context.fetch(fetchRequest).first {
                     // Update the properties of the existing entry
                     existingEntry.name = contactImageEntry.name
                     existingEntry.layout = contactImageEntry.layout.rawValue
@@ -136,8 +143,8 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
                     existingEntry.fontWeight = contactImageEntry.fontWeight.asString
                     existingEntry.fontWidth = contactImageEntry.fontWidth.asString
 
-                    guard self.backgroundContext.hasChanges else { return }
-                    try self.backgroundContext.save()
+                    guard context.hasChanges else { return }
+                    try context.save()
                 } else {
                     debugPrint(
                         "\(DebuggingIdentifiers.failed) \(#file) \(#function) No matching Contact Trick Entry found to update."

+ 17 - 9
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -28,9 +28,10 @@ struct TDDResult {
 final class BaseTDDStorage: TDDStorage, Injectable {
     @Injected() private var storage: FileStorage!
 
-    private let privateContext = CoreDataStack.shared.newTaskContext()
+    private let makeContext: () -> NSManagedObjectContext
 
-    init(resolver: Resolver) {
+    init(resolver: Resolver, contextProvider: (() -> NSManagedObjectContext)? = nil) {
+        makeContext = contextProvider ?? { CoreDataStack.shared.newTaskContext() }
         injectServices(resolver)
     }
 
@@ -144,8 +145,10 @@ final class BaseTDDStorage: TDDStorage, Injectable {
     /// Stores the Total Daily Dose (TDD) result in Core Data
     /// - Parameter tddResult: The TDD result to store, containing total insulin, bolus, temp basal, scheduled basal and weighted average
     func storeTDD(_ tddResult: TDDResult) async {
-        await privateContext.perform {
-            let tddStored = TDDStored(context: self.privateContext)
+        let context = makeContext()
+        context.name = "storeTDD"
+        await context.perform {
+            let tddStored = TDDStored(context: context)
             tddStored.id = UUID()
             tddStored.date = Date()
             tddStored.total = NSDecimalNumber(decimal: tddResult.total)
@@ -155,8 +158,8 @@ final class BaseTDDStorage: TDDStorage, Injectable {
             tddStored.weightedAverage = tddResult.weightedAverage.map { NSDecimalNumber(decimal: $0) }
 
             do {
-                guard self.privateContext.hasChanges else { return }
-                try self.privateContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch {
                 debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to save TDD: \(error)")
             }
@@ -570,14 +573,17 @@ final class BaseTDDStorage: TDDStorage, Injectable {
 
         let predicate = NSPredicate(format: "date >= %@", tenDaysAgo as NSDate)
 
+        let context = makeContext()
+        context.name = "calculateWeightedAverage"
+
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TDDStored.self,
-            onContext: privateContext,
+            onContext: context,
             predicate: predicate,
             key: "date",
             ascending: false
         )
-        return await privateContext.perform { () -> Decimal? in
+        return await context.perform { () -> Decimal? in
             guard let results = results as? [TDDStored], !results.isEmpty else { return 0 }
 
             // Calculate recent (2h) average
@@ -616,7 +622,9 @@ final class BaseTDDStorage: TDDStorage, Injectable {
     /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
     /// - Throws: An error if the Core Data count operation fails.
     func hasSufficientTDD() async throws -> Bool {
-        try await BaseTDDStorage.hasSufficientTDD(context: privateContext)
+        let context = makeContext()
+        context.name = "hasSufficientTDD"
+        return try await BaseTDDStorage.hasSufficientTDD(context: context)
     }
 
     /// internal function with context exposed to enable testing

+ 0 - 2
Trio/Sources/Modules/History/HistoryStateModel.swift

@@ -14,8 +14,6 @@ extension History {
         @ObservationIgnored @Injected() var healthKitManager: HealthKitManager!
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
 
-        let coredataContext = CoreDataStack.shared.newTaskContext()
-
         var mode: Mode = .treatments
         var treatments: [Treatment] = []
         var manualGlucose: Decimal = 0

+ 8 - 5
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -21,9 +21,6 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     @Injected() private var fileStorage: FileStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
 
-    let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
-    let determinationFetchContext = CoreDataStack.shared.newTaskContext()
-
     init(resolver: Resolver) {
         injectServices(resolver)
     }
@@ -191,16 +188,18 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: glucoseFetchContext,
+            onContext: context,
             predicate: NSPredicate.glucose,
             key: "date",
             ascending: false,
             fetchLimit: 288
         )
 
-        return try await glucoseFetchContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -313,6 +312,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             let maxCOB = preferences.maxCOB
 
             // Fetch glucose data
+            let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
+            glucoseFetchContext.name = "handleBolusCalculation.glucose"
             let glucoseIds = try await fetchGlucose()
             let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared.getNSManagedObject(
                 with: glucoseIds,
@@ -323,6 +324,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             }
 
             // Fetch determination data
+            let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+            determinationFetchContext.name = "handleBolusCalculation.determination"
             let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
                 predicate: NSPredicate.predicateFor30MinAgoForDetermination
             )

+ 8 - 5
Trio/Sources/Services/Calendar/CalendarManager.swift

@@ -77,7 +77,6 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         registerSubscribers()
     }
 
-    let backgroundContext = CoreDataStack.shared.newTaskContext()
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
     private func setupCurrentCalendar() {
@@ -177,9 +176,11 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     }
 
     private func getLastDetermination() async throws -> NSManagedObjectID? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "getLastDetermination"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             key: "timestamp",
             ascending: false,
@@ -187,7 +188,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [[String: Any]] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -197,16 +198,18 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     }
 
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.predicateFor30MinAgo,
             key: "date",
             ascending: false,
             propertiesToFetch: ["objectID", "glucose"]
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [[String: Any]] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

+ 8 - 5
Trio/Sources/Services/ContactImage/ContactImageManager.swift

@@ -31,7 +31,6 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     private(set) var state = ContactImageState()
 
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
     // Queue for handling Core Data change notifications
     private let queue = DispatchQueue(label: "BaseContactImageManager.queue", qos: .background)
@@ -101,16 +100,18 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     // MARK: - Core Data Fetches
 
     private func fetchlastDetermination() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchlastDetermination"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(format: "deliverAt >= %@", Date.halfHourAgo as NSDate), // fetches enacted and suggested
             key: "deliverAt",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OrefDetermination] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -120,16 +121,18 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     }
 
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.predicateFor20MinAgo,
             key: "date",
             ascending: false,
             fetchLimit: 3 /// We only need 1-3 values, depending on whether the user wants to show delta or not
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

+ 22 - 16
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -55,8 +55,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
 
-    private var backgroundContext = CoreDataStack.shared.newTaskContext()
-
     // Queue for handling Core Data change notifications
     private let queue = DispatchQueue(label: "BaseHealthKitManager.queue", qos: .background)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
@@ -214,19 +212,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateGlucoseAsUploaded"
+        await context.perform {
             let ids = glucose.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToHealth = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
@@ -335,19 +335,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateCarbsAsUploaded"
+        await context.perform {
             let ids = carbs.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToHealth = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
@@ -377,9 +379,11 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               insulinEvents.isNotEmpty else { return }
 
         do {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "uploadInsulin"
             let fetchedInsulinEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
                 ofType: PumpEventStored.self,
-                onContext: backgroundContext,
+                onContext: context,
                 predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
                     NSPredicate.pumpHistoryLast24h,
                     NSPredicate(format: "tempBasal != nil")
@@ -391,7 +395,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
 
             var insulinSamples: [HKQuantitySample] = []
 
-            try await backgroundContext.perform {
+            try await context.perform {
                 guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else {
                     throw CoreDataError.fetchError(function: #function, file: #file)
                 }
@@ -582,19 +586,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateInsulinAsUploaded"
+        await context.perform {
             let ids = insulin.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToHealth = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"

+ 2 - 1
Trio/Sources/Services/IOB/IOBService.swift

@@ -34,7 +34,6 @@ final class BaseIOBService: IOBService, Injectable {
     private var subscriptions = Set<AnyCancellable>()
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private let queue = DispatchQueue(label: "BaseIOBService.queue", qos: .background)
-    private let context = CoreDataStack.shared.newTaskContext()
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -64,6 +63,8 @@ final class BaseIOBService: IOBService, Injectable {
     private func fetchLatestDeterminationIOB() -> (iob: Decimal?, date: Date?) {
         var iob: Decimal?
         var date: Date?
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchLatestDeterminationIOB"
         context.performAndWait {
             let request = OrefDetermination.fetchRequest() as NSFetchRequest<OrefDetermination>
             request.sortDescriptors = [NSSortDescriptor(key: "deliverAt", ascending: false)]

+ 7 - 0
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 
 // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
@@ -5,6 +6,8 @@ import Foundation
 @available(iOS 16.2, *)
 extension LiveActivityManager {
     func fetchAndMapGlucose() async throws -> [GlucoseData] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchAndMapGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
@@ -27,6 +30,8 @@ extension LiveActivityManager {
 
     // TODO: extract logic or at least rename function appropiately
     func fetchAndMapDetermination() async throws -> DeterminationData? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchAndMapDetermination"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
@@ -68,6 +73,8 @@ extension LiveActivityManager {
     }
 
     func fetchAndMapOverride() async throws -> OverrideData? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchAndMapOverride"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: context,

+ 0 - 3
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -69,9 +69,6 @@ final class LiveActivityData: ObservableObject {
 
     private var data = LiveActivityData()
 
-    /// A Core Data task context.
-    let context = CoreDataStack.shared.newTaskContext()
-
     /// A dispatch queue for handling Core Data change notifications.
     private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?

+ 62 - 43
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -47,9 +47,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     // Queue where upload pipelines run.
     let uploadPipelineQueue = DispatchQueue(label: "NightscoutManager.uploadPipelines", qos: .utility)
 
-    // Background Core Data context for fetches used by upload tasks.
-    var backgroundContext = CoreDataStack.shared.newTaskContext()
-
     /// Throttle window (seconds) per upload pipeline. Any requests inside this window
     /// coalesce into a single upload run for that pipeline.
     let uploadPipelineInterval: [NightscoutUploadPipeline: TimeInterval] = [
@@ -353,9 +350,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func fetchBattery() async -> Battery {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchBattery"
+        return await context.perform {
             do {
-                let results = try self.backgroundContext.fetch(OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo))
+                let results = try context.fetch(OpenAPS_Battery.fetch(NSPredicate.predicateFor30MinAgo))
                 if let last = results.first {
                     let percent: Int? = Int(last.percent)
                     let voltage: Decimal? = last.voltage as Decimal?
@@ -411,16 +410,18 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             return
         }
 
+        let tddContext = CoreDataStack.shared.newTaskContext()
+        tddContext.name = "uploadDeviceStatus.tdd"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TDDStored.self,
-            onContext: backgroundContext,
+            onContext: tddContext,
             predicate: NSPredicate.predicateFor30MinAgo,
             key: "date",
             ascending: false,
             fetchLimit: 1
         )
 
-        let tdd: Decimal? = await backgroundContext.perform {
+        let tdd: Decimal? = await tddContext.perform {
             (results as? [TDDStored])?.first?.total as? Decimal
         }
 
@@ -589,19 +590,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateOrefDeterminationAsUploaded(_ determination: [Determination]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateOrefDeterminationAsUploaded"
+        await context.perform {
             let ids = determination.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -882,19 +885,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateGlucoseAsUploaded"
+        await context.perform {
             let ids = glucose.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -938,19 +943,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updatePumpEventStoredsAsUploaded(_ treatments: [NightscoutTreatment]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updatePumpEventStoredsAsUploaded"
+        await context.perform {
             let ids = treatments.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -979,19 +986,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateManualGlucoseAsUploaded(_ treatments: [NightscoutTreatment]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateManualGlucoseAsUploaded"
+        await context.perform {
             let ids = treatments.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -1020,19 +1029,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateCarbsAsUploaded(_ treatments: [NightscoutTreatment]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateCarbsAsUploaded"
+        await context.perform {
             let ids = treatments.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -1079,19 +1090,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateOverridesAsUploaded(_ overrides: [NightscoutExercise]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateOverridesAsUploaded"
+        await context.perform {
             let ids = overrides.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -1137,19 +1150,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateOverrideRunsAsUploaded(_ overrideRuns: [NightscoutExercise]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateOverrideRunsAsUploaded"
+        await context.perform {
             let ids = overrideRuns.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<OverrideRunStored> = OverrideRunStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
@@ -1178,19 +1193,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateTempTargetsAsUploaded(_ tempTargets: [NightscoutTreatment]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateTempTargetsAsUploaded"
+        await context.perform {
             let ids = tempTargets.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS for TempTargetStored: \(error.userInfo)"
@@ -1219,19 +1236,21 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     private func updateTempTargetRunsAsUploaded(_ tempTargetRuns: [NightscoutTreatment]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateTempTargetRunsAsUploaded"
+        await context.perform {
             let ids = tempTargetRuns.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<TempTargetRunStored> = TempTargetRunStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToNS = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS for TempTargetRunStored: \(error.userInfo)"

+ 90 - 86
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -39,8 +39,6 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }
     }
 
-    private var backgroundContext = CoreDataStack.shared.newTaskContext()
-
     // Queue for handling Core Data change notifications
     private let queue = DispatchQueue(label: "BaseTidepoolManager.queue", qos: .background)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
@@ -221,19 +219,21 @@ extension BaseTidepoolManager {
     }
 
     private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateCarbsAsUploaded"
+        await context.perform {
             let ids = carbs.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToTidepool = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
@@ -291,10 +291,12 @@ extension BaseTidepoolManager {
         guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
         do {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "uploadDose"
             // Fetch all temp basal entries from Core Data for the last 24 hours
             let results = try await CoreDataStack.shared.fetchEntitiesAsync(
                 ofType: PumpEventStored.self,
-                onContext: backgroundContext,
+                onContext: context,
                 predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
                     NSPredicate.pumpHistoryLast24h,
                     NSPredicate(format: "tempBasal != nil")
@@ -305,7 +307,7 @@ extension BaseTidepoolManager {
             )
 
             // Ensure that the processing happens within the background context for thread safety
-            try await backgroundContext.perform {
+            try await context.perform {
                 guard let existingTempBasalEntries = results as? [PumpEventStored] else {
                     throw CoreDataError.fetchError(function: #function, file: #file)
                 }
@@ -407,19 +409,21 @@ extension BaseTidepoolManager {
     }
 
     private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateInsulinAsUploaded"
+        await context.perform {
             let ids = insulin.map(\.id) as NSArray
             let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToTidepool = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"
@@ -461,79 +465,77 @@ extension BaseTidepoolManager {
     ) -> [DoseEntry] {
         var insulinDoseEvents: [DoseEntry] = []
 
-        backgroundContext.performAndWait {
-            // Loop through the pump history events within the background context
-            guard let duration = event.duration, let amount = event.amount,
-                  let currentBasalRate = self.getCurrentBasalRate()
-            else {
-                return
-            }
-            let value = (Decimal(duration) / 60.0) * amount
-
-            // Find the corresponding temp basal entry in existingTempBasalEntries
-            if let matchingEntryIndex = existingTempBasalEntries.firstIndex(where: { $0.timestamp == event.timestamp }) {
-                // Check for a predecessor (the entry before the matching entry)
-                let predecessorIndex = matchingEntryIndex - 1
-                if predecessorIndex >= 0 {
-                    let predecessorEntry = existingTempBasalEntries[predecessorIndex]
-                    if let predecessorTimestamp = predecessorEntry.timestamp,
-                       let predecessorEntrySyncIdentifier = predecessorEntry.id
-                    {
-                        let predecessorEndDate = predecessorTimestamp
-                            .addingTimeInterval(TimeInterval(
-                                Int(predecessorEntry.tempBasal?.duration ?? 0) *
-                                    60
-                            )) // parse duration to minutes
-
-                        // If the predecessor's end date is later than the current event's start date, adjust it
-                        if predecessorEndDate > event.timestamp {
-                            let adjustedEndDate = event.timestamp
-                            let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
-                            let adjustedDeliveredUnits = (adjustedDuration / 3600) *
-                                Double(truncating: predecessorEntry.tempBasal?.rate ?? 0)
-
-                            // Create updated predecessor dose entry
-                            let updatedPredecessorEntry = DoseEntry(
-                                type: .tempBasal,
-                                startDate: predecessorTimestamp,
-                                endDate: adjustedEndDate,
-                                value: adjustedDeliveredUnits,
-                                unit: .units,
-                                deliveredUnits: adjustedDeliveredUnits,
-                                syncIdentifier: predecessorEntrySyncIdentifier,
-                                insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
-                                automatic: true,
-                                manuallyEntered: false,
-                                isMutable: false
-                            )
-                            // Add the updated predecessor entry to the result
-                            insulinDoseEvents.append(updatedPredecessorEntry)
-                        }
+        // Caller (uploadDose) already executes within context.perform, so we run directly here
+        guard let duration = event.duration, let amount = event.amount,
+              let currentBasalRate = self.getCurrentBasalRate()
+        else {
+            return insulinDoseEvents
+        }
+        let value = (Decimal(duration) / 60.0) * amount
+
+        // Find the corresponding temp basal entry in existingTempBasalEntries
+        if let matchingEntryIndex = existingTempBasalEntries.firstIndex(where: { $0.timestamp == event.timestamp }) {
+            // Check for a predecessor (the entry before the matching entry)
+            let predecessorIndex = matchingEntryIndex - 1
+            if predecessorIndex >= 0 {
+                let predecessorEntry = existingTempBasalEntries[predecessorIndex]
+                if let predecessorTimestamp = predecessorEntry.timestamp,
+                   let predecessorEntrySyncIdentifier = predecessorEntry.id
+                {
+                    let predecessorEndDate = predecessorTimestamp
+                        .addingTimeInterval(TimeInterval(
+                            Int(predecessorEntry.tempBasal?.duration ?? 0) *
+                                60
+                        )) // parse duration to minutes
+
+                    // If the predecessor's end date is later than the current event's start date, adjust it
+                    if predecessorEndDate > event.timestamp {
+                        let adjustedEndDate = event.timestamp
+                        let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
+                        let adjustedDeliveredUnits = (adjustedDuration / 3600) *
+                            Double(truncating: predecessorEntry.tempBasal?.rate ?? 0)
+
+                        // Create updated predecessor dose entry
+                        let updatedPredecessorEntry = DoseEntry(
+                            type: .tempBasal,
+                            startDate: predecessorTimestamp,
+                            endDate: adjustedEndDate,
+                            value: adjustedDeliveredUnits,
+                            unit: .units,
+                            deliveredUnits: adjustedDeliveredUnits,
+                            syncIdentifier: predecessorEntrySyncIdentifier,
+                            insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                            automatic: true,
+                            manuallyEntered: false,
+                            isMutable: false
+                        )
+                        // Add the updated predecessor entry to the result
+                        insulinDoseEvents.append(updatedPredecessorEntry)
                     }
                 }
-
-                // Create a new dose entry for the current event
-                let currentEndDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration)))
-                let newDoseEntry = DoseEntry(
-                    type: .tempBasal,
-                    startDate: event.timestamp,
-                    endDate: currentEndDate,
-                    value: Double(value),
-                    unit: .units,
-                    deliveredUnits: Double(value),
-                    syncIdentifier: event.id,
-                    scheduledBasalRate: HKQuantity(
-                        unit: .internationalUnitsPerHour,
-                        doubleValue: Double(currentBasalRate.rate)
-                    ),
-                    insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
-                    automatic: true,
-                    manuallyEntered: false,
-                    isMutable: false
-                )
-                // Add the new event entry to the result
-                insulinDoseEvents.append(newDoseEntry)
             }
+
+            // Create a new dose entry for the current event
+            let currentEndDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(duration)))
+            let newDoseEntry = DoseEntry(
+                type: .tempBasal,
+                startDate: event.timestamp,
+                endDate: currentEndDate,
+                value: Double(value),
+                unit: .units,
+                deliveredUnits: Double(value),
+                syncIdentifier: event.id,
+                scheduledBasalRate: HKQuantity(
+                    unit: .internationalUnitsPerHour,
+                    doubleValue: Double(currentBasalRate.rate)
+                ),
+                insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                automatic: true,
+                manuallyEntered: false,
+                isMutable: false
+            )
+            // Add the new event entry to the result
+            insulinDoseEvents.append(newDoseEntry)
         }
 
         return insulinDoseEvents
@@ -626,19 +628,21 @@ extension BaseTidepoolManager {
     }
 
     private func updateGlucoseAsUploaded(_ glucose: [StoredGlucoseSample]) async {
-        await backgroundContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateGlucoseAsUploaded"
+        await context.perform {
             let ids = glucose.map(\.syncIdentifier) as NSArray
             let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try context.fetch(fetchRequest)
                 for result in results {
                     result.isUploadedToTidepool = true
                 }
 
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard context.hasChanges else { return }
+                try context.save()
             } catch let error as NSError {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToTidepool: \(error.userInfo)"

+ 3 - 1
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -81,13 +81,15 @@ extension TrioRemoteControl {
     }
 
     private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchTotalRecentBolusAmount"
         let predicate = NSPredicate(
             format: "type == %@ AND timestamp > %@",
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
         )
         let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self, onContext: pumpHistoryFetchContext, predicate: predicate, key: "timestamp",
+            ofType: PumpEventStored.self, onContext: context, predicate: predicate, key: "timestamp",
             ascending: true, fetchLimit: nil, propertiesToFetch: ["bolus.amount"]
         )
         guard let bolusDictionaries = results as? [[String: Any]] else {

+ 0 - 2
Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift

@@ -14,11 +14,9 @@ class TrioRemoteControl: Injectable {
 
     private let timeWindow: TimeInterval = 600
 
-    internal let pumpHistoryFetchContext: NSManagedObjectContext
     internal let viewContext: NSManagedObjectContext
 
     private init() {
-        pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
         viewContext = CoreDataStack.shared.persistentContainer.viewContext
         injectServices(TrioApp.resolver)
     }

+ 4 - 3
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -76,7 +76,6 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private var lifetime = Lifetime()
 
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
 
     // Queue for handling Core Data change notifications
     private let queue = DispatchQueue(label: "BaseUserNotificationsManager.queue", qos: .userInitiated)
@@ -272,16 +271,18 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     }
 
     private func fetchGlucoseIDs() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucoseIDs"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.predicateFor20MinAgo,
             key: "date",
             ascending: false,
             fetchLimit: 3
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

+ 19 - 11
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -41,7 +41,6 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     typealias PumpEvent = PumpEventStored.EventType
 
-    let backgroundContext = CoreDataStack.shared.newTaskContext()
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
     init(resolver: Resolver) {
@@ -184,6 +183,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             return WatchState(date: Date())
         }
         do {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "setupWatchState"
+
             // Get NSManagedObjectIDs
             let glucoseIds = try await fetchGlucose()
             let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
@@ -194,15 +196,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
             // Get NSManagedObjects
             let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
-                .getNSManagedObject(with: glucoseIds, context: backgroundContext)
+                .getNSManagedObject(with: glucoseIds, context: context)
             let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
-                .getNSManagedObject(with: determinationIds, context: backgroundContext)
+                .getNSManagedObject(with: determinationIds, context: context)
             let overridePresetObjects: [OverrideStored] = try await CoreDataStack.shared
-                .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
+                .getNSManagedObject(with: overridePresetIds, context: context)
             let tempTargetPresetObjects: [TempTargetStored] = try await CoreDataStack.shared
-                .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
+                .getNSManagedObject(with: tempTargetPresetIds, context: context)
 
-            return await backgroundContext.perform {
+            return await context.perform {
                 var watchState = WatchState(date: Date())
 
                 // Set lastLoopDate
@@ -374,16 +376,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.glucose,
             key: "date",
             ascending: false,
             fetchLimit: 288
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -395,16 +399,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     /// Fetches last pump event that is a non-external bolus from CoreData
     /// - Returns: NSManagedObjectIDs for last bolus
     func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchLastBolus"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastPumpBolus,
             key: "timestamp",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [PumpEventStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -642,13 +648,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     guard let self = self else { return }
 
                     do {
+                        let context = CoreDataStack.shared.newTaskContext()
+                        context.name = "requestBolusRecommendation"
                         // Fetch determination data
                         let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
                             predicate: NSPredicate.predicateFor30MinAgoForDetermination
                         )
                         let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared.getNSManagedObject(
                             with: determinationIds,
-                            context: backgroundContext
+                            context: context
                         )
 
                         await MainActor.run {

+ 14 - 10
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -156,9 +156,6 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Additional local subscriptions (separate from `cancellables`) for CoreData events.
     private var subscriptions = Set<AnyCancellable>()
 
-    /// Represents the context for background tasks in CoreData.
-    let backgroundContext = CoreDataStack.shared.newTaskContext()
-
     /// Represents the main (view) context for CoreData, typically used on the main thread.
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -454,16 +451,18 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// - Parameter limit: Maximum number of glucose entries to fetch (default: 2)
     /// - Returns: An array of `NSManagedObjectID`s for glucose readings.
     private func fetchGlucose(limit: Int = 2) async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchGlucose"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.glucose,
             key: "date",
             ascending: false,
             fetchLimit: limit
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -474,6 +473,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Fetches the most recent temporary basal rate from CoreData pump history.
     /// - Returns: An array containing the NSManagedObjectID of the latest temp basal event, if any.
     private func fetchTempBasals() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchTempBasals"
         let tempBasalPredicate = NSPredicate(format: "tempBasal != nil")
         let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
             NSPredicate.pumpHistoryLast24h,
@@ -482,14 +483,14 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
 
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: compoundPredicate,
             key: "timestamp",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let pumpEvents = results as? [PumpEventStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -501,16 +502,18 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Returns them sorted newest first, allowing us to find both enacted and suggested determinations.
     /// - Returns: An array of `NSManagedObjectID`s for all determinations in the 30-minute window.
     private func fetchDeterminations30Min() async throws -> [NSManagedObjectID] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchDeterminations30Min"
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             key: "deliverAt",
             ascending: false,
             fetchLimit: 0 // No limit - get all determinations in 30min window
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OrefDetermination] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -556,7 +559,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
         let previousWatchState = lastPreparedWatchState
 
         // Capture context locally for use in perform block
-        let context = backgroundContext
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "setupGarminWatchState"
 
         let watchStates = await context.perform {
             // Fetch Core Data objects inside perform block

+ 0 - 1
Trio/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -20,7 +20,6 @@ import Swinject
 
     let resolver: Resolver
 
-    let coredataContext = CoreDataStack.shared.newTaskContext()
     let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
     override init() {

+ 15 - 6
Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift

@@ -18,14 +18,17 @@ import UIKit
      - Throws: An error if fetching fails or Core Data operations fail.
      */
     func fetchAndProcessOverrides() async throws -> [OverridePreset] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchAndProcessOverrides"
+
         do {
             // Fetch all Override Presets via OverrideStorage
             let allOverridePresetsIDs = try await overrideStorage.fetchForOverridePresets()
 
             // Since we are fetching on a different background Thread we need to unpack the NSManagedObjectID on the correct Thread first
-            return try await coredataContext.perform {
+            return try await context.perform {
                 let overrideObjects = try allOverridePresetsIDs.compactMap { id in
-                    try self.coredataContext.existingObject(with: id) as? OverrideStored
+                    try context.existingObject(with: id) as? OverrideStored
                 }
 
                 return overrideObjects.map { object in
@@ -51,12 +54,15 @@ import UIKit
      - Throws: `overridePresetsError.noTempOverrideFound` if no presets are found.
      */
     func fetchIDs(_ uuid: [OverridePreset.ID]) async throws -> [OverridePreset] {
-        try await coredataContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchIDs"
+
+        return try await context.perform {
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", uuid)
 
             do {
-                let result = try self.coredataContext.fetch(fetchRequest)
+                let result = try context.fetch(fetchRequest)
 
                 if result.isEmpty {
                     debug(
@@ -87,12 +93,15 @@ import UIKit
      - Throws: `overridePresetsError.noTempOverrideFound` if the preset is not found.
      */
     private func fetchOverrideID(_ preset: OverridePreset) async throws -> NSManagedObjectID {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchOverrideID"
+
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
         fetchRequest.fetchLimit = 1
 
-        return try await coredataContext.perform {
-            guard let objectID = try self.coredataContext.fetch(fetchRequest).first?.objectID else {
+        return try await context.perform {
+            guard let objectID = try context.fetch(fetchRequest).first?.objectID else {
                 debug(
                     .default,
                     "\(DebuggingIdentifiers.failed) No override found for preset: \(preset.name)"

+ 0 - 2
Trio/Sources/Shortcuts/State/StateIntentRequest.swift

@@ -55,8 +55,6 @@ struct StateBGQuery: EntityQuery {
 }
 
 final class StateIntentRequest: BaseIntentsRequest {
-    let moc = CoreDataStack.shared.newTaskContext()
-
     func getLastGlucose(onContext: NSManagedObjectContext) throws
         -> (dateGlucose: Date, glucose: String, trend: String, delta: String)
     {

+ 18 - 7
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -18,14 +18,17 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
     /// - Returns: An array of `TempPreset` objects.
     /// - Throws: An error if fetching or processing fails.
     func fetchAndProcessTempTargets() async throws -> [TempPreset] {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchAndProcessTempTargets"
+
         // Fetch all Temp Target Presets via TempTargetStorage
         let allTempTargetPresetsIDs = try await tempTargetsStorage.fetchForTempTargetPresets()
 
         // Perform the fetch and process on the Core Data context's thread
-        return try await coredataContext.perform {
+        return try await context.perform {
             // Fetch existing TempTargetStored objects based on their NSManagedObjectIDs
             let tempTargetObjects: [TempTargetStored] = allTempTargetPresetsIDs.compactMap { id in
-                guard let object = try? self.coredataContext.existingObject(with: id) as? TempTargetStored else {
+                guard let object = try? context.existingObject(with: id) as? TempTargetStored else {
                     debugPrint("\(#file) \(#function) Failed to fetch object for ID: \(id)")
                     return nil
                 }
@@ -52,12 +55,15 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
     /// - Parameter uuid: An array of preset IDs to fetch.
     /// - Returns: An array of `TempPreset` objects.
     func fetchIDs(_ uuid: [TempPreset.ID]) async -> [TempPreset] {
-        await coredataContext.perform {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchIDs"
+
+        return await context.perform {
             let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", uuid)
 
             do {
-                let result = try self.coredataContext.fetch(fetchRequest)
+                let result = try context.fetch(fetchRequest)
 
                 if result.isEmpty {
                     debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) No TempTargetStored found for ids: \(uuid)")
@@ -84,14 +90,19 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
     ///
     /// - Parameter preset: The `TempPreset` to find.
     /// - Returns: The `NSManagedObjectID` of the temp target if found, otherwise `nil`.
-    private func fetchTempTargetID(_ preset: TempPreset) async -> NSManagedObjectID? {
+    private func fetchTempTargetID(_ preset: TempPreset) async ->
+        NSManagedObjectID?
+    {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchTempTargetID"
+
         let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id.uuidString)
         fetchRequest.fetchLimit = 1
 
-        return await coredataContext.perform {
+        return await context.perform {
             do {
-                return try self.coredataContext.fetch(fetchRequest).first?.objectID
+                return try context.fetch(fetchRequest).first?.objectID
             } catch {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Target: \(error)"