Browse Source

Tear down Treatments FRC pipelines and in-flight work on dismissal

cleanupTreatmentState() left the FRCs wired and the setup task captured
self through a full oref simulation, so a dismissed instance kept
recomputing on every viewContext merge. Nil the closures and cancel
tracked tasks in cleanup, guard callbacks on isActive, and funnel the
insulin/forecast recompute through one tracked, cancellable task.
Marvin Polscheit 1 ngày trước cách đây
mục cha
commit
bb959485e6
1 tập tin đã thay đổi với 37 bổ sung12 xóa
  1. 37 12
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

+ 37 - 12
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -211,6 +211,10 @@ extension Treatments {
 
         private var hasCleanedUp = false
 
+        /// In-flight work started by this instance; cancelled in `cleanupTreatmentState()`.
+        @ObservationIgnored private var setupTask: Task<Void, Never>?
+        @ObservationIgnored private var determinationUpdateTask: Task<Void, Never>?
+
         func cleanupTreatmentState() {
             guard !hasCleanedUp else { return }
             hasCleanedUp = true
@@ -218,6 +222,16 @@ extension Treatments {
             unsubscribe()
             lifetime = Lifetime()
 
+            // Stop the FRC → recompute pipelines; a dismissed instance must not keep
+            // re-running the bolus calculator on every viewContext merge.
+            glucoseControllerDelegate.onContentChange = nil
+            determinationControllerDelegate.onContentChange = nil
+            lastBolusControllerDelegate.onContentChange = nil
+
+            // Cancel in-flight work — the setup task awaits a full oref simulation.
+            setupTask?.cancel()
+            determinationUpdateTask?.cancel()
+
             broadcaster?.unregister(DeterminationObserver.self, observer: self)
             broadcaster?.unregister(BolusFailureObserver.self, observer: self)
 
@@ -226,7 +240,7 @@ extension Treatments {
 
         private func setupBolusStateConcurrently() {
             debug(.bolusState, "Setting up bolus state concurrently...")
-            Task {
+            setupTask = Task {
                 // Load settings and observers first so the determination controller's initial
                 // population (which runs calculateInsulin) sees correct values.
                 do {
@@ -248,6 +262,8 @@ extension Treatments {
                     debug(.default, "Failed to setup bolus state concurrently: \(error)")
                 }
 
+                guard !Task.isCancelled else { return }
+
                 // NSFetchedResultsControllers are bound to the viewContext, so set them up on the main actor.
                 await self.setupGlucoseController()
                 await self.setupDeterminationController()
@@ -773,7 +789,8 @@ extension Treatments.StateModel {
     @MainActor func setupGlucoseController() {
         glucoseControllerDelegate.onContentChange = { [weak self] in
             Task { @MainActor in
-                self?.updateGlucoseFromController()
+                guard let self, self.isActive else { return }
+                self.updateGlucoseFromController()
             }
         }
 
@@ -820,27 +837,34 @@ extension Treatments.StateModel {
     @MainActor func setupDeterminationController() {
         determinationControllerDelegate.onContentChange = { [weak self] in
             Task { @MainActor in
-                guard let self else { return }
+                guard let self, self.isActive else { return }
                 self.updateDeterminationFromController()
-                self.insulinCalculated = await self.calculateInsulin()
-                let forecastData = self.mapForecastsFromController()
-                await self.updateForecasts(with: forecastData)
+                self.scheduleInsulinAndForecastUpdate()
             }
         }
 
         do {
             try determinationController.performFetch()
             updateDeterminationFromController()
-            Task { @MainActor in
-                self.insulinCalculated = await self.calculateInsulin()
-                let forecastData = self.mapForecastsFromController()
-                await self.updateForecasts(with: forecastData)
-            }
+            scheduleInsulinAndForecastUpdate()
         } catch {
             debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform determination fetch: \(error)")
         }
     }
 
+    /// Recomputes bolus recommendation and forecast; each new determination cancels the
+    /// previous in-flight run so a superseded run cannot publish stale results.
+    @MainActor private func scheduleInsulinAndForecastUpdate() {
+        determinationUpdateTask?.cancel()
+        determinationUpdateTask = Task { @MainActor in
+            let insulinCalculated = await self.calculateInsulin()
+            guard !Task.isCancelled else { return }
+            self.insulinCalculated = insulinCalculated
+            let forecastData = self.mapForecastsFromController()
+            await self.updateForecasts(with: forecastData)
+        }
+    }
+
     @MainActor private func updateDeterminationFromController() {
         guard let objects = determinationController.fetchedObjects,
               let mostRecentDetermination = objects.first else { return }
@@ -975,7 +999,8 @@ extension Treatments.StateModel {
     @MainActor func setupLastBolusController() {
         lastBolusControllerDelegate.onContentChange = { [weak self] in
             Task { @MainActor in
-                self?.updateLastBolusFromController()
+                guard let self, self.isActive else { return }
+                self.updateLastBolusFromController()
             }
         }