Skip to content
Open
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-05-18 - Dictionary overhead vs O(n) array lookups
**Learning:** Avoid replacing O(n) `firstIndex(where:)` searches with O(1) dictionary-based index maps for SwiftUI `@Published` arrays if the collection is small, the interaction is infrequent, or the map requires frequent reconstruction (e.g., on every scan). The O(n) map construction overhead (allocating memory and hashing every UUID) will outweigh the lookup benefits.
**Action:** When evaluating lookup optimizations, explicitly weigh the frequency of the lookup against the frequency of the dataset being rebuilt. Use index maps only for large, long-lived datasets with extremely frequent, hot-path lookups.
17 changes: 15 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 scanResultsIndexMap: [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
scanResultsIndexMap = 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,11 @@ class CacheoutViewModel: ObservableObject {
}

func toggleSelection(for id: UUID) {
if let index = scanResults.firstIndex(where: { $0.id == id }) {
// Try O(1) map lookup first
if let index = scanResultsIndexMap[id], index < scanResults.count, scanResults[index].id == id {
scanResults[index].isSelected.toggle()
} else if let index = scanResults.firstIndex(where: { $0.id == id }) {
// Fallback to O(n) search if map is desynced
scanResults[index].isSelected.toggle()
}
}
Expand All @@ -191,7 +200,11 @@ class CacheoutViewModel: ObservableObject {
// MARK: - Node Modules selection

func toggleNodeModulesSelection(for id: UUID) {
if let i = nodeModulesItems.firstIndex(where: { $0.id == id }) {
// Try O(1) map lookup first
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 }) {
// Fallback to O(n) search if map is desynced
nodeModulesItems[i].isSelected.toggle()
}
}
Expand Down