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

Replace Core Data publisher also in TreatmentsStateModel

Marvin Polscheit недель назад: 2
Родитель
Сommit
356941192e

+ 218 - 1
Trio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,6 +1,169 @@
 {
+  "originHash" : "b271d5de8862534ec5ccf9ccfd7ce226afa7ed4c799b7066ccbe2281782402a1",
   "pins" : [
     {
+      "identity" : "abseil-cpp-binary",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/abseil-cpp-binary.git",
+      "state" : {
+        "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+        "version" : "1.2024072200.0"
+      }
+    },
+    {
+      "identity" : "app-check",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/app-check.git",
+      "state" : {
+        "revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
+        "version" : "11.2.0"
+      }
+    },
+    {
+      "identity" : "bluecryptor",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueCryptor.git",
+      "state" : {
+        "revision" : "cec97c24b111351e70e448972a7d3fe68a756d6d",
+        "version" : "2.0.2"
+      }
+    },
+    {
+      "identity" : "blueecc",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueECC.git",
+      "state" : {
+        "revision" : "1485268a54f8135435a825a855e733f026fa6cc8",
+        "version" : "1.2.201"
+      }
+    },
+    {
+      "identity" : "bluersa",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/BlueRSA.git",
+      "state" : {
+        "revision" : "f40325520344a966523b214394aa350132a6af68",
+        "version" : "1.0.203"
+      }
+    },
+    {
+      "identity" : "connectiq-companion-app-sdk-ios",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/garmin/connectiq-companion-app-sdk-ios",
+      "state" : {
+        "revision" : "f0d29ff691d700a132d86205ed9bb091e336c2f7",
+        "version" : "1.8.0"
+      }
+    },
+    {
+      "identity" : "cryptoswift",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/krzyzanowskim/CryptoSwift",
+      "state" : {
+        "revision" : "f2a627b84c1ff96f21ac2fcb623ab36142dd5512",
+        "version" : "1.10.0"
+      }
+    },
+    {
+      "identity" : "firebase-ios-sdk",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/firebase/firebase-ios-sdk.git",
+      "state" : {
+        "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5",
+        "version" : "11.15.0"
+      }
+    },
+    {
+      "identity" : "google-ads-on-device-conversion-ios-sdk",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+      "state" : {
+        "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed",
+        "version" : "2.3.0"
+      }
+    },
+    {
+      "identity" : "googleappmeasurement",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/GoogleAppMeasurement.git",
+      "state" : {
+        "revision" : "45ce435e9406d3c674dd249a042b932bee006f60",
+        "version" : "11.15.0"
+      }
+    },
+    {
+      "identity" : "googledatatransport",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/GoogleDataTransport.git",
+      "state" : {
+        "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+        "version" : "10.1.0"
+      }
+    },
+    {
+      "identity" : "googleutilities",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/GoogleUtilities.git",
+      "state" : {
+        "revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
+        "version" : "8.1.0"
+      }
+    },
+    {
+      "identity" : "grpc-binary",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/grpc-binary.git",
+      "state" : {
+        "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+        "version" : "1.69.1"
+      }
+    },
+    {
+      "identity" : "gtm-session-fetcher",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/gtm-session-fetcher.git",
+      "state" : {
+        "revision" : "c756a29784521063b6a1202907e2cc47f41b667c",
+        "version" : "4.5.0"
+      }
+    },
+    {
+      "identity" : "interop-ios-for-google-sdks",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+      "state" : {
+        "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+        "version" : "101.0.0"
+      }
+    },
+    {
+      "identity" : "kituracontracts",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/KituraContracts.git",
+      "state" : {
+        "revision" : "6edf7ac3dd2b3a2c61284778d430bbad7d8a6f23",
+        "version" : "2.0.1"
+      }
+    },
+    {
+      "identity" : "leveldb",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/firebase/leveldb.git",
+      "state" : {
+        "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+        "version" : "1.22.5"
+      }
+    },
+    {
+      "identity" : "loggerapi",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Kitura/LoggerAPI.git",
+      "state" : {
+        "revision" : "4e6b45e850ffa275e8e26a24c6454fd709d5b6ac",
+        "version" : "2.0.0"
+      }
+    },
+    {
       "identity" : "mkringprogressview",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
@@ -10,6 +173,33 @@
       }
     },
     {
+      "identity" : "nanopb",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/firebase/nanopb.git",
+      "state" : {
+        "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
+        "version" : "2.30910.0"
+      }
+    },
+    {
+      "identity" : "promises",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/google/promises.git",
+      "state" : {
+        "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
+        "version" : "2.4.0"
+      }
+    },
+    {
+      "identity" : "slidebutton",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/no-comment/SlideButton",
+      "state" : {
+        "branch" : "main",
+        "revision" : "5eacebba4d7deeb693592bc9a62ab2d2181e133b"
+      }
+    },
+    {
       "identity" : "swift-algorithms",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-algorithms",
@@ -19,6 +209,24 @@
       }
     },
     {
+      "identity" : "swift-jwt",
+      "kind" : "remoteSourceControl",
+      "location" : "http://github.com/Kitura/Swift-JWT.git",
+      "state" : {
+        "revision" : "f68ec28fbd90a651597e9e825ea7f315f8d52a1f",
+        "version" : "4.0.1"
+      }
+    },
+    {
+      "identity" : "swift-log",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-log.git",
+      "state" : {
+        "revision" : "5073617dac96330a486245e4c0179cb0a6fd2256",
+        "version" : "1.12.0"
+      }
+    },
+    {
       "identity" : "swift-numerics",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-numerics",
@@ -28,6 +236,15 @@
       }
     },
     {
+      "identity" : "swift-protobuf",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/apple/swift-protobuf.git",
+      "state" : {
+        "revision" : "81558271e243f8f47dfe8e9fdd55f3c2b5413f68",
+        "version" : "1.37.0"
+      }
+    },
+    {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
@@ -64,5 +281,5 @@
       }
     }
   ],
-  "version" : 2
+  "version" : 3
 }

+ 167 - 223
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -110,7 +110,6 @@ extension Treatments {
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var predictionsForChart: Predictions?
         var simulatedDetermination: Determination?
-        @MainActor var determinationObjectIDs: [NSManagedObjectID] = []
 
         var minForecast: [Int] = []
         var maxForecast: [Int] = []
@@ -128,9 +127,61 @@ extension Treatments {
         var showDeterminationFailureAlert = false
         var determinationFailureMessage = ""
 
-        // Queue for handling Core Data change notifications
-        private let queue = DispatchQueue(label: "TreatmentsStateModel.queue", qos: .userInitiated)
-        private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
+        // MARK: - NSFetchedResultsControllers
+
+        //
+        // Glucose, the latest determination and the last pump bolus are driven by
+        // NSFetchedResultsControllers bound to the viewContext. They keep their `fetchedObjects`
+        // continuously in sync and notify us through their delegate's `onContentChange` closure.
+
+        @ObservationIgnored let glucoseControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var glucoseController: NSFetchedResultsController<GlucoseStored> = {
+            let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
+            request.predicate = NSPredicate.glucose
+            request.fetchBatchSize = 50
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = glucoseControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let determinationControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var determinationController: NSFetchedResultsController<OrefDetermination> = {
+            let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
+            request.predicate = NSPredicate.predicateFor30MinAgoForDetermination
+            request.fetchLimit = 1
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = determinationControllerDelegate
+            return controller
+        }()
+
+        @ObservationIgnored let lastBolusControllerDelegate = FetchedResultsControllerDelegate()
+        @ObservationIgnored private(set) lazy var lastBolusController: NSFetchedResultsController<PumpEventStored> = {
+            let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
+            request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: false)]
+            request.predicate = NSPredicate.lastPumpBolus
+            request.fetchLimit = 1
+            let controller = NSFetchedResultsController(
+                fetchRequest: request,
+                managedObjectContext: viewContext,
+                sectionNameKeyPath: nil,
+                cacheName: nil
+            )
+            controller.delegate = lastBolusControllerDelegate
+            return controller
+        }()
+
         private var subscriptions = Set<AnyCancellable>()
 
         typealias PumpEvent = PumpEventStored.EventType
@@ -150,13 +201,6 @@ extension Treatments {
             }
 
             debug(.bolusState, "subscribe fired")
-            coreDataPublisher =
-                changedObjectsOnManagedObjectContextDidSavePublisher()
-                    .receive(on: queue)
-                    .share()
-                    .eraseToAnyPublisher()
-            registerHandlers()
-            registerSubscribers()
             setupBolusStateConcurrently()
             subscribeToBolusProgress()
         }
@@ -183,13 +227,12 @@ extension Treatments {
         private func setupBolusStateConcurrently() {
             debug(.bolusState, "Setting up bolus state concurrently...")
             Task {
+                // Load settings and observers first so the determination controller's initial
+                // population (which runs calculateInsulin) sees correct values.
                 do {
                     try await withThrowingTaskGroup(of: Void.self) { group in
                         group.addTask {
-                            self.setupGlucoseArray()
-                        }
-                        group.addTask {
-                            self.setupDeterminationsAndForecasts()
+                            await self.getAllSettingsValues()
                         }
                         group.addTask {
                             await self.setupSettings()
@@ -197,9 +240,6 @@ extension Treatments {
                         group.addTask {
                             self.registerObservers()
                         }
-                        group.addTask {
-                            self.setupLastBolus()
-                        }
 
                         // Wait for all tasks to complete
                         try await group.waitForAll()
@@ -207,6 +247,11 @@ extension Treatments {
                 } catch let error as NSError {
                     debug(.default, "Failed to setup bolus state concurrently: \(error)")
                 }
+
+                // NSFetchedResultsControllers are bound to the viewContext, so set them up on the main actor.
+                await self.setupGlucoseController()
+                await self.setupDeterminationController()
+                await self.setupLastBolusController()
             }
         }
 
@@ -266,20 +311,6 @@ extension Treatments {
             }
         }
 
-        private func setupDeterminationsAndForecasts() {
-            Task {
-                async let getAllSettingsDefaults: () = getAllSettingsValues()
-                async let setupDeterminations: () = setupDeterminationsArray()
-
-                await getAllSettingsDefaults
-                await setupDeterminations
-
-                // Determination has updated, so we can use this to draw the initial Forecast Chart
-                let forecastData = await mapForecastsForChart()
-                await updateForecasts(with: forecastData)
-            }
-        }
-
         private func registerObservers() {
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(BolusFailureObserver.self, observer: self)
@@ -729,82 +760,29 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
     }
 }
 
-extension Treatments.StateModel {
-    private func registerHandlers() {
-        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task {
-                await self.setupDeterminationsArray()
-                let forecastData = await self.mapForecastsForChart()
-                await self.updateForecasts(with: forecastData)
-            }
-        }.store(in: &subscriptions)
-
-        // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            self.setupGlucoseArray()
-        }.store(in: &subscriptions)
-
-        // Refresh `lastPumpBolus` whenever a new pump event lands (mirrors HomeStateModel)
-        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
-            self?.setupLastBolus()
-        }.store(in: &subscriptions)
-    }
-
-    private func registerSubscribers() {
-        glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupGlucoseArray()
-            }
-            .store(in: &subscriptions)
-    }
-}
-
-// MARK: - Setup Glucose and Determinations
+// MARK: - Setup Glucose, Determinations and Last Bolus
 
 extension Treatments.StateModel {
-    // Glucose
-    private func setupGlucoseArray() {
-        Task {
-            do {
-                let ids = try await self.fetchGlucose()
-                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
-                    .getNSManagedObject(with: ids, context: viewContext)
-                await updateGlucoseArray(with: glucoseObjects)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error)"
-                )
-            }
-        }
-    }
-
-    private func fetchGlucose() async throws -> [NSManagedObjectID] {
-        let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
-        glucoseFetchContext.name = "TreatmentsStateModel.fetchGlucose"
+    // MARK: - Glucose Controller
 
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: glucoseFetchContext,
-            predicate: NSPredicate.glucose,
-            key: "date",
-            ascending: false
-        )
-
-        return try await glucoseFetchContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
+    @MainActor func setupGlucoseController() {
+        glucoseControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateGlucoseFromController()
             }
+        }
 
-            return fetchedResults.map(\.objectID)
+        do {
+            try glucoseController.performFetch()
+            updateGlucoseFromController()
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform glucose fetch: \(error)")
         }
     }
 
-    @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
+    @MainActor private func updateGlucoseFromController() {
+        guard let objects = glucoseController.fetchedObjects else { return }
+
         // Store all objects for the forecast graph
         glucoseFromPersistence = objects
 
@@ -832,107 +810,84 @@ extension Treatments.StateModel {
         deltaBG = delta
     }
 
-    // Determinations
-    private func setupDeterminationsArray() async {
-        do {
-            let fetchedObjectIDs = try await determinationStorage.fetchLastDeterminationObjectID(
-                predicate: NSPredicate.predicateFor30MinAgoForDetermination
-            )
+    // MARK: - Determination Controller
 
-            await MainActor.run {
-                determinationObjectIDs = fetchedObjectIDs
+    @MainActor func setupDeterminationController() {
+        determinationControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                guard let self else { return }
+                self.updateDeterminationFromController()
+                self.insulinCalculated = await self.calculateInsulin()
+                let forecastData = self.mapForecastsFromController()
+                await self.updateForecasts(with: forecastData)
             }
+        }
 
-            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
-                .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
-
-            updateDeterminationsArray(with: determinationObjects)
-        } catch let error as CoreDataError {
-            debug(.default, "Core Data error: \(error)")
+        do {
+            try determinationController.performFetch()
+            updateDeterminationFromController()
+            Task { @MainActor in
+                self.insulinCalculated = await self.calculateInsulin()
+                let forecastData = self.mapForecastsFromController()
+                await self.updateForecasts(with: forecastData)
+            }
         } catch {
-            debug(.default, "Unexpected error: \(error)")
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform determination fetch: \(error)")
         }
     }
 
-    private func mapForecastsForChart() async -> Determination? {
-        guard let determinationID = await MainActor.run(body: { determinationObjectIDs.first }) else {
+    @MainActor private func updateDeterminationFromController() {
+        guard let objects = determinationController.fetchedObjects,
+              let mostRecentDetermination = objects.first else { return }
+
+        determination = objects
+
+        // setup vars for bolus calculation
+        insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
+        evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
+        minPredBG = (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal
+        lastLoopDate = apsManager.lastLoopDate as Date?
+        insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
+        target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
+        isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
+        cob = mostRecentDetermination.cob as Int16
+        iob = (mostRecentDetermination.iob ?? 0) as Decimal
+        basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
+        carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
+    }
+
+    @MainActor private func mapForecastsFromController() -> Determination? {
+        guard let determinationObject = determinationController.fetchedObjects?.first else {
             return nil
         }
 
-        let context = CoreDataStack.shared.newTaskContext()
-        context.name = "TreatmentsStateModel.mapForecastsForChart"
-
-        return await context.perform {
-            let request = NSFetchRequest<Forecast>(entityName: "Forecast")
-            request.predicate = NSPredicate(format: "orefDetermination = %@", determinationID)
-            request.relationshipKeyPathsForPrefetching = ["forecastValues"]
-
-            let forecasts: [Forecast]
-            do {
-                forecasts = try context.fetch(request)
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error)"
-                )
-                return nil
-            }
-
-            func values(for type: String) -> [Int]? {
-                let result = forecasts.first { $0.type == type }?
-                    .forecastValuesArray
-                    .map { Int($0.value) }
-                return (result?.isEmpty ?? true) ? nil : result
-            }
-
-            let predictions = Predictions(
-                iob: values(for: "iob"),
-                zt: values(for: "zt"),
-                cob: values(for: "cob"),
-                uam: values(for: "uam")
-            )
-
-            return Determination(
-                id: UUID(),
-                reason: "",
-                units: 0,
-                insulinReq: 0,
-                sensitivityRatio: 0,
-                rate: 0,
-                duration: 0,
-                iob: 0,
-                cob: 0,
-                predictions: predictions.isEmpty ? nil : predictions,
-                carbsReq: 0,
-                temp: nil,
-                reservoir: 0,
-                insulinForManualBolus: 0,
-                manualBolusErrorString: 0,
-                carbRatio: 0,
-                received: false
-            )
-        }
-    }
+        let forecastsSet = determinationObject.forecasts ?? []
+        let predictions = Predictions(
+            iob: forecastsSet.extractValues(for: "iob"),
+            zt: forecastsSet.extractValues(for: "zt"),
+            cob: forecastsSet.extractValues(for: "cob"),
+            uam: forecastsSet.extractValues(for: "uam")
+        )
 
-    private func updateDeterminationsArray(with objects: [OrefDetermination]) {
-        Task { @MainActor in
-            guard let mostRecentDetermination = objects.first else { return }
-            determination = objects
-
-            // setup vars for bolus calculation
-            insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
-            evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
-            minPredBG = (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal
-            lastLoopDate = apsManager.lastLoopDate as Date?
-            insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
-            target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
-            isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
-            cob = mostRecentDetermination.cob as Int16
-            iob = (mostRecentDetermination.iob ?? 0) as Decimal
-            basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
-            carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
-            insulinCalculated = await calculateInsulin()
-        }
+        return Determination(
+            id: UUID(),
+            reason: "",
+            units: 0,
+            insulinReq: 0,
+            sensitivityRatio: 0,
+            rate: 0,
+            duration: 0,
+            iob: 0,
+            cob: 0,
+            predictions: predictions.isEmpty ? nil : predictions,
+            carbsReq: 0,
+            temp: nil,
+            reservoir: 0,
+            insulinForManualBolus: 0,
+            manualBolusErrorString: 0,
+            carbRatio: 0,
+            received: false
+        )
     }
 }
 
@@ -1011,46 +966,35 @@ private extension Predictions {
 // MARK: - Last Pump Bolus
 
 extension Treatments.StateModel {
-    /// Mirrors `HomeStateModel.setupLastBolus` so the in-progress visualizer can show the
+    /// Mirrors `HomeStateModel`'s last-bolus controller so the in-progress visualizer can show the
     /// running pump-bolus's amount as the denominator (not the user's pending entry).
     /// Filters out external boluses via `NSPredicate.lastPumpBolus`.
-    func setupLastBolus() {
-        Task {
-            do {
-                guard let id = try await fetchLastBolus() else { return }
-                await updateLastBolus(with: id)
-            } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error)")
-            }
-        }
-    }
-
-    private func fetchLastBolus() async throws -> NSManagedObjectID? {
-        let context = CoreDataStack.shared.newTaskContext()
-        context.name = "fetchLastBolus"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: context,
-            predicate: NSPredicate.lastPumpBolus,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1
-        )
-
-        return try await context.perform {
-            guard let fetched = results as? [PumpEventStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
+    @MainActor func setupLastBolusController() {
+        lastBolusControllerDelegate.onContentChange = { [weak self] in
+            Task { @MainActor in
+                self?.updateLastBolusFromController()
             }
-            return fetched.map(\.objectID).first
         }
-    }
 
-    @MainActor private func updateLastBolus(with id: NSManagedObjectID) {
         do {
-            lastPumpBolus = try viewContext.existingObject(with: id) as? PumpEventStored
+            try lastBolusController.performFetch()
+            updateLastBolusFromController()
         } catch {
-            debug(.default, "\(DebuggingIdentifiers.failed) updateLastBolus: \(error)")
+            debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform last bolus fetch: \(error)")
         }
     }
+
+    @MainActor private func updateLastBolusFromController() {
+        lastPumpBolus = lastBolusController.fetchedObjects?.first
+    }
+}
+
+private extension Set where Element == Forecast {
+    /// Extracts the sorted forecast values for a given prediction type (iob/zt/cob/uam).
+    func extractValues(for type: String) -> [Int]? {
+        let values = first { $0.type == type }?
+            .forecastValuesArray
+            .map { Int($0.value) }
+        return (values?.isEmpty ?? true) ? nil : values
+    }
 }