From accb2b9372cb9f53a3eb331c9fddbbfdd618ca1a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 04:12:34 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Bolt:=20Optimize=20selecti?= =?UTF-8?q?on=20toggles=20with=20O(1)=20index=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ Sources/Cacheout/ViewModels/CacheoutViewModel.swift | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index dd1b466..f20f6da 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,3 +21,7 @@ ## 2026-03-19 - FileManager Enumerator Pre-fetching **Learning:** Any property requested via `resourceValues(forKeys:)` inside a `FileManager.enumerator` loop must also be in `includingPropertiesForKeys` — otherwise `URL.resourceValues` falls back to a synchronous `stat()` per file, turning bulk reads into O(N) disk I/O. **Action:** Keep the keys array passed to `resourceValues(forKeys:)` a subset of the prefetch list passed to `FileManager.enumerator(at:includingPropertiesForKeys:)`. + +## 2024-06-25 - O(1) Index Maps for @Published Array Toggles +**Learning:** When toggling the selection state of an item within a massive `@Published` array (like `nodeModulesItems`), sequentially searching for the element's index via `firstIndex(where:)` causes O(n) CPU latency on every user click. +**Action:** Replace `firstIndex(where:)` with an O(1) dictionary-based index map (`[UUID: Int]`). Rebuild the dictionary via `enumerated().reduce(into: [:])` immediately after the main array is fully populated (e.g., after a scan operation completes), and include a defensive boundary and ID check fallback for robustness. diff --git a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift index 0e8464b..60998b7 100644 --- a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift +++ b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift @@ -54,6 +54,9 @@ class CacheoutViewModel: ObservableObject { @Published var nodeModulesItems: [NodeModulesItem] = [] @Published var isNodeModulesScanning = false + private var scanResultIndexMap: [UUID: Int] = [:] + private var nodeModulesIndexMap: [UUID: Int] = [:] + /// Increments on every completed scan — views can use .task(id:) to react @Published var scanGeneration: Int = 0 @@ -154,9 +157,11 @@ class CacheoutViewModel: ObservableObject { async let nmResults = nodeModulesScanner.scan() scanResults = await cacheResults + scanResultIndexMap = scanResults.enumerated().reduce(into: [:]) { $0[$1.element.id] = $1.offset } isScanning = false nodeModulesItems = await nmResults + nodeModulesIndexMap = nodeModulesItems.enumerated().reduce(into: [:]) { $0[$1.element.id] = $1.offset } isNodeModulesScanning = false // Track scan completion for reactive UI updates @@ -166,7 +171,9 @@ class CacheoutViewModel: ObservableObject { } func toggleSelection(for id: UUID) { - if let index = scanResults.firstIndex(where: { $0.id == id }) { + if let index = scanResultIndexMap[id], index < scanResults.count, scanResults[index].id == id { + scanResults[index].isSelected.toggle() + } else if let index = scanResults.firstIndex(where: { $0.id == id }) { scanResults[index].isSelected.toggle() } } @@ -191,7 +198,9 @@ class CacheoutViewModel: ObservableObject { // MARK: - Node Modules selection func toggleNodeModulesSelection(for id: UUID) { - if let i = nodeModulesItems.firstIndex(where: { $0.id == id }) { + if let i = nodeModulesIndexMap[id], i < nodeModulesItems.count, nodeModulesItems[i].id == id { + nodeModulesItems[i].isSelected.toggle() + } else if let i = nodeModulesItems.firstIndex(where: { $0.id == id }) { nodeModulesItems[i].isSelected.toggle() } }