From e1d11d43f64ffdf416a3392170d96d027d279f5a Mon Sep 17 00:00:00 2001 From: InteractionEngineer Date: Thu, 14 May 2026 21:12:37 +0200 Subject: [PATCH 1/3] unvalidated fix: reduce excessive Cospend API requests when adding projects validatedInput and validatedServer were computed vars, causing 3 independent Combine chains and 2 network requests per debounce cycle. Converting them to lazy stored properties with .share() reduces this to 1 request. Also bumped password debounce from 0.5s to 1s and added .removeDuplicates() to the QR scanner publisher. --- .../Manual/AddProjectManualViewModel.swift | 44 ++++++++++--------- .../QRCodes/AddProjectQRViewModel.swift | 4 +- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift index 777370c..f50066d 100644 --- a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift +++ b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift @@ -128,10 +128,10 @@ class AddProjectManualViewModel: ObservableObject { .eraseToAnyPublisher() } - var validatedInput: AnyPublisher { - return Publishers.CombineLatest3(validatedAddress, $projectName, $projectPassword) + lazy var validatedInput: AnyPublisher = { + Publishers.CombineLatest3(validatedAddress, $projectName, $projectPassword) .debounce(for: 1, scheduler: DispatchQueue.main) - .compactMap { server, token, password in + .compactMap { server, token, password -> Project? in if let address = server.address, address.isValidURL, !token.isEmpty, !password.isEmpty { guard let url = URL(string: address) else { return nil } return Project(name: token, password: password, token: token, backend: server.0, url: url) @@ -140,28 +140,30 @@ class AddProjectManualViewModel: ObservableObject { } } .removeDuplicates() + .share() .eraseToAnyPublisher() - } - - private var validatedServer: AnyPublisher { - validatedInput.flatMap { - project in - Future { promise in - Task { - do { - let testedProject = try await NetworkService.shared.getProjectName(project) - self.lastProjectTestedSuccessfully = testedProject - promise(.success(200)) - } catch { - promise(.success(-1)) + }() + + private lazy var validatedServer: AnyPublisher = { + validatedInput + .flatMap(maxPublishers: .max(1)) { project in + Future { promise in + Task { + do { + let testedProject = try await NetworkService.shared.getProjectName(project) + self.lastProjectTestedSuccessfully = testedProject + promise(.success(200)) + } catch { + promise(.success(-1)) + } } } } - } - .removeDuplicates() - .receive(on: RunLoop.main) - .eraseToAnyPublisher() - } + .removeDuplicates() + .receive(on: RunLoop.main) + .share() + .eraseToAnyPublisher() + }() private var errorTextPublisher: AnyPublisher { validatedServer diff --git a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift index b355047..d4bf9c3 100644 --- a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift +++ b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift @@ -44,13 +44,12 @@ class AddProjectQRViewModel: ObservableObject { .compactMap { $0 }, $name, $passwordText - .debounce(for: 0.5, scheduler: RunLoop.main) + .debounce(for: 1, scheduler: RunLoop.main) .compactMap { $0.isEmpty ? nil : $0 } .removeDuplicates() ) .map { url, token, password in self.isTestingSubject.send(.connecting) - print("\(url) \(token) \(password)") return Project(name: "", password: password, token: token, backend: .cospend, url: url) } .flatMap { project in @@ -86,6 +85,7 @@ class AddProjectQRViewModel: ObservableObject { var foundCode: AnyPublisher { $scannedCode .compactMap { $0 } + .removeDuplicates() .eraseToAnyPublisher() } From 9574db52b43d609f0b7a70e71c1447d9ce31d527 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 19:35:05 +0000 Subject: [PATCH 2/3] fix: use switchToLatest for latest-only validation in AddProjectManualViewModel Replace flatMap(maxPublishers: .max(1)) with map + switchToLatest() so that when a new project input arrives, the previous in-flight validation is abandoned and its result is never delivered downstream. Move lastProjectTestedSuccessfully assignment into handleEvents after switchToLatest so stale async completions cannot enable the Add button with outdated data. Agent-Logs-Url: https://github.com/InteractionEngineer/PayForMe/sessions/6439287b-270d-45c7-b76d-2544c0f0689c Co-authored-by: InteractionEngineer <56229275+InteractionEngineer@users.noreply.github.com> --- .../Manual/AddProjectManualViewModel.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift index f50066d..defa90e 100644 --- a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift +++ b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift @@ -146,21 +146,26 @@ class AddProjectManualViewModel: ObservableObject { private lazy var validatedServer: AnyPublisher = { validatedInput - .flatMap(maxPublishers: .max(1)) { project in - Future { promise in + .map { project -> AnyPublisher<(Project?, Int), Never> in + Future<(Project?, Int), Never> { promise in Task { do { let testedProject = try await NetworkService.shared.getProjectName(project) - self.lastProjectTestedSuccessfully = testedProject - promise(.success(200)) + promise(.success((testedProject, 200))) } catch { - promise(.success(-1)) + promise(.success((nil, -1))) } } } + .eraseToAnyPublisher() } - .removeDuplicates() + .switchToLatest() .receive(on: RunLoop.main) + .handleEvents(receiveOutput: { (project, _) in + self.lastProjectTestedSuccessfully = project + }) + .map { (_, statusCode) in statusCode } + .removeDuplicates() .share() .eraseToAnyPublisher() }() From 78aabbf82eb7a1e6f65d2b600c2468b9efcc10c6 Mon Sep 17 00:00:00 2001 From: Jona Date: Thu, 14 May 2026 21:38:16 +0200 Subject: [PATCH 3/3] re-allow for duplicates in case of re-scan Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift index d4bf9c3..1456a04 100644 --- a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift +++ b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift @@ -85,7 +85,6 @@ class AddProjectQRViewModel: ObservableObject { var foundCode: AnyPublisher { $scannedCode .compactMap { $0 } - .removeDuplicates() .eraseToAnyPublisher() }