Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 11 additions & 2 deletions Sources/Cacheout/ViewModels/CacheoutViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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()
}
}
Expand All @@ -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()
}
}
Expand Down