diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift index 6c058763..ab2b8199 100644 --- a/Bitkit/Services/BackupService.swift +++ b/Bitkit/Services/BackupService.swift @@ -180,7 +180,7 @@ class BackupService { } VssStoreIdProvider.shared.clearCache() - VssBackupClient.shared.reset() + await VssBackupClient.shared.reset() Logger.debug("Full restore starting", context: "BackupService") @@ -504,8 +504,6 @@ class BackupService { func getLatestBackupTime() async -> UInt64? { do { - try await vssBackupClient.setup() - let timestamps = await withTaskGroup(of: UInt64?.self) { group in for category in BackupCategory.allCases where category != .lightningConnections { group.addTask { diff --git a/Bitkit/Services/VssBackupClient.swift b/Bitkit/Services/VssBackupClient.swift index cfb4d247..ee83c2ee 100644 --- a/Bitkit/Services/VssBackupClient.swift +++ b/Bitkit/Services/VssBackupClient.swift @@ -1,18 +1,69 @@ import Foundation import VssRustClientFfi +/// Actor to coordinate VSS client setup (ensures only one setup runs at a time) +private actor VssSetupCoordinator { + private enum SetupState { + case idle + case inProgress(Task) + case completed + } + + private var state: SetupState = .idle + + func awaitSetup(setupAction: @escaping () async throws -> Void) async throws { + switch state { + case .completed: + Logger.debug("VssSetupCoordinator: already completed, returning", context: "VssBackupClient") + return + + case let .inProgress(existingTask): + Logger.debug("VssSetupCoordinator: setup in progress, waiting for existing task", context: "VssBackupClient") + try await existingTask.value + Logger.debug("VssSetupCoordinator: existing task completed", context: "VssBackupClient") + return + + case .idle: + Logger.debug("VssSetupCoordinator: idle, starting new setup", context: "VssBackupClient") + let task = Task { + try await setupAction() + } + state = .inProgress(task) + + do { + try await task.value + state = .completed + Logger.debug("VssSetupCoordinator: setup completed successfully", context: "VssBackupClient") + } catch { + // Reset on any error to allow retry attempts + state = .idle + Logger.debug("VssSetupCoordinator: setup failed, resetting to idle", context: "VssBackupClient") + throw error + } + } + } + + func reset() { + Logger.debug("VssSetupCoordinator: reset called", context: "VssBackupClient") + if case let .inProgress(task) = state { + task.cancel() + } + state = .idle + } +} + class VssBackupClient { static let shared = VssBackupClient() - private var isSetup: Task? + private let setupCoordinator = VssSetupCoordinator() private init() {} - func reset() { - isSetup = nil + func reset() async { + await setupCoordinator.reset() } - func setup(walletIndex: Int = 0) async throws { + private func setup(walletIndex: Int = 0) async throws { do { try await withTimeout(seconds: 30) { Logger.debug("VSS client setting up…", context: "VssBackupClient") @@ -87,26 +138,9 @@ class VssBackupClient { } private func awaitSetup() async throws { - if let existingSetup = isSetup { - do { - try await existingSetup.value - } catch let error as CancellationError { - isSetup = nil - throw error - } - } - - let setupTask = Task { + try await setupCoordinator.awaitSetup { [self] in try await setup() } - isSetup = setupTask - - do { - try await setupTask.value - } catch let error as CancellationError { - isSetup = nil - throw error - } } private func withTimeout(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T { diff --git a/Bitkit/Utilities/AppReset.swift b/Bitkit/Utilities/AppReset.swift index d76fe6ed..a47a3e9b 100644 --- a/Bitkit/Utilities/AppReset.swift +++ b/Bitkit/Utilities/AppReset.swift @@ -17,7 +17,7 @@ enum AppReset { // Stop backup observers and reset VSS client await BackupService.shared.stopObservingBackups() - VssBackupClient.shared.reset() + await VssBackupClient.shared.reset() // Stop node and wipe LDK persistence via the wallet API. try await wallet.wipe()