From 000f866f1b1b4d3b215cbe807a4cb8398ea2361d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 2 Nov 2026 15:36:00 -0600 Subject: [PATCH] Await alert retractions before asking Loop to delete the manager retractAllAlerts() is async. delete() awaits it and then cgmManagerWantsDeletion; discardSensor() runs it in a Task. The settings view awaits delete() before dismissing. --- .../LibreLoopCGMManager/LibreLoopCGMManager.swift | 10 ++++------ LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift | 10 +++------- LibreLoopTests/LibreLoopCGMManagerStateTests.swift | 7 +++---- .../LibreLoopUICoordinator.swift | 13 +++++-------- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/LibreLoop/LibreLoopCGMManager/LibreLoopCGMManager.swift b/LibreLoop/LibreLoopCGMManager/LibreLoopCGMManager.swift index dc4cef4..f135e52 100644 --- a/LibreLoop/LibreLoopCGMManager/LibreLoopCGMManager.swift +++ b/LibreLoop/LibreLoopCGMManager/LibreLoopCGMManager.swift @@ -359,7 +359,7 @@ public final class LibreLoopCGMManager: CGMManager { recentSamples = [] hasIssuedReScanAlert = false lastSensorAttention = nil - retractAllAlerts() + Task { await retractAllAlerts() } // Emit .sensorEnd before we blank state so the event's // deviceIdentifier still resolves to the session that's ending. // Matches the .sensorStart we emitted at pairing time so Loop's @@ -848,7 +848,7 @@ public final class LibreLoopCGMManager: CGMManager { } } - public func delete(completion: @escaping () -> Void) { + public func delete() async { // The central is shared and outlives this manager, so a deleted manager // must stop driving it — otherwise its event listener + reconnect loop // keep issuing connect/scan on the shared central and fight whatever @@ -876,10 +876,8 @@ public final class LibreLoopCGMManager: CGMManager { scanner.cancelConnection(peripheral) } } - // Retract while the delegate reference is still good: notifying it - // releases this manager. - retractAllAlerts() - notifyDelegateOfDeletion(completion: completion) + await retractAllAlerts() + await cgmManagerDelegate?.cgmManagerWantsDeletion(self) } } diff --git a/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift b/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift index 16fc8c7..6a48f2f 100644 --- a/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift +++ b/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift @@ -46,18 +46,14 @@ extension LibreLoopCGMManager { return activatedAt } - /// Captures the delegate strongly so the retraction still lands if this - /// manager is released immediately afterwards. - func retractAllAlerts() { + func retractAllAlerts() async { let delegate = cgmManagerDelegate let identifiers = Self.allAlertIdentifiers.map { Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: $0) } llog("alerts: retracting \(identifiers.count) identifier(s)") - Task { - for identifier in identifiers { - await delegate?.retractAlert(identifier: identifier) - } + for identifier in identifiers { + await delegate?.retractAlert(identifier: identifier) } } diff --git a/LibreLoopTests/LibreLoopCGMManagerStateTests.swift b/LibreLoopTests/LibreLoopCGMManagerStateTests.swift index 9a60066..53194b6 100644 --- a/LibreLoopTests/LibreLoopCGMManagerStateTests.swift +++ b/LibreLoopTests/LibreLoopCGMManagerStateTests.swift @@ -138,17 +138,16 @@ final class LibreLoopAlertRetractionTests: XCTestCase { }) } - func testDeleteRetractsEveryAlertAndNotifiesDelegate() { + func testDeleteRetractsEveryAlertBeforeNotifyingDelegate() async { let retractions = expectRetractions() let deletion = expectation(description: "delegate notified of deletion") - let completed = expectation(description: "delete completion called") let delegate = RetractionRecordingDelegate(retractionExpectation: retractions, deletionExpectation: deletion) let manager = makeManager(delegate: delegate) - manager.delete { completed.fulfill() } + await manager.delete() - wait(for: [retractions, deletion, completed], timeout: 5) + await fulfillment(of: [retractions, deletion], timeout: 0, enforceOrder: true) assertRetractedEverything(delegate) } diff --git a/LibreLoopUI/LibreLoopCGMManager/LibreLoopUICoordinator.swift b/LibreLoopUI/LibreLoopCGMManager/LibreLoopUICoordinator.swift index d00eaf0..8eefdfe 100644 --- a/LibreLoopUI/LibreLoopCGMManager/LibreLoopUICoordinator.swift +++ b/LibreLoopUI/LibreLoopCGMManager/LibreLoopUICoordinator.swift @@ -166,15 +166,12 @@ final class LibreLoopUICoordinator: UINavigationController, CGMManagerOnboarding self.completionDelegate?.completionNotifyingDidComplete(self) }, replaceSensor: { [weak self] in self?.startReplacementPairing() }, - // `delete` runs the manager's teardown and notifies the delegate - // itself; `notifyDelegateOfDeletion` alone skips the teardown. deleteCGM: { [weak self] in - self?.cgmManager?.delete { - DispatchQueue.main.async { - guard let self = self else { return } - self.completionDelegate?.completionNotifyingDidComplete(self) - self.dismiss(animated: true) - } + Task { + await self?.cgmManager?.delete() + guard let self = self else { return } + self.completionDelegate?.completionNotifyingDidComplete(self) + self.dismiss(animated: true) } } )