@@ -17,6 +17,8 @@ final class QueryViewModel {
17 // Build search texts asynchronously
17 // Build search texts asynchronously
18 searchTextTask?.cancel()
18 searchTextTask?.cancel()
19 cachedSearchTexts = []
19 cachedSearchTexts = []
20+ searchTextGeneration &+= 1
21+ let generation = searchTextGeneration
20 // Apply the current filter immediately using the inline fallback path
22 // Apply the current filter immediately using the inline fallback path
21 // (the cache was just invalidated). Calling updateFilteredResults()
23 // (the cache was just invalidated). Calling updateFilteredResults()
22 // rather than `filteredResults = results` avoids briefly showing
24 // rather than `filteredResults = results` avoids briefly showing
@@ -36,6 +38,12 @@ final class QueryViewModel {
38 guard let self, !Task.isCancelled else { return }
40 guard let self, !Task.isCancelled else { return }
41+ // Reject a stale write: if `results` was reassigned while this task
42+ // was awaiting, a newer didSet bumped the generation. Task.isCancelled
43+ // alone isn't sufficient because two re-queries producing result sets
44+ // of identical count would otherwise let this stale, same-length
45+ // cachedSearchTexts slip past updateFilteredResults's count guard.
46+ guard generation == self.searchTextGeneration else { return }
39 self.cachedSearchTexts = texts
47 self.cachedSearchTexts = texts
40 if !self.filterText.isEmpty {
48 if !self.filterText.isEmpty {
41 self.updateFilteredResults()
49 self.updateFilteredResults()
@@ -45,6 +53,9 @@ final class QueryViewModel {
46 private var cachedSearchTexts: [String] = []
54 private var cachedSearchTexts: [String] = []
47 private var searchTextTask: Task<Void, Never>?
55 private var searchTextTask: Task<Void, Never>?
56+ /// Bumped on every `results` reassignment so the async search-text builder can
57+ /// detect it was superseded and drop a stale write (see results.didSet).
58+ private var searchTextGeneration = 0
48 var columns: [String] = []
59 var columns: [String] = []
49 var columnOrder: [String] = []
60 var columnOrder: [String] = []
50 var hiddenColumns: Set<String> = []
61 var hiddenColumns: Set<String> = []