Skip to content

Commit ffd0522

Browse files
committed
fix(search): implement cache invalidation for failed search index builds
1 parent 0d58ced commit ffd0522

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Bug Fixes
66

77
* Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up.
8-
* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. A failed restore from the cached index is no longer reported as a successful load.
8+
* Disable the search controls and explain why when the search index cannot be built, instead of leaving the spinner running for as long as the page is open. A failed restore from the cached index is discarded instead of being reported as a successful load or retried after every reload.
99

1010
## v5.0.0 (2026-08-06)
1111

attack-search/__tests__/search-events.test.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,14 @@ describe('search event bindings', () => {
129129
});
130130

131131
test('a failed restore from the cache is not reported as a successful load', async () => {
132-
await loadIndexWithAFailingWarmRestore();
132+
const { cacheKey, deleteCachedDatabase } = await loadIndexWithAFailingWarmRestore();
133133

134134
// The catch used to set the loaded flag false and the finally set it straight back to
135135
// true, so `search` went on to query an index that was never populated.
136136
expect(mockJqueryApis['#search-input'].prop).toHaveBeenCalledWith('disabled', true);
137137
expect(mockJqueryApis['#search-icon'].addClass).toHaveBeenCalledWith('error-icon');
138+
expect(global.localStorage.removeItem).toHaveBeenCalledWith(cacheKey);
139+
expect(deleteCachedDatabase).toHaveBeenCalledTimes(1);
138140
});
139141

140142
test('a failed index build puts the search controls into their unavailable state', async () => {
@@ -172,18 +174,26 @@ async function loadIndexWithAFailingColdStart() {
172174
async function loadIndexWithAFailingWarmRestore() {
173175
const { searchCacheCompatibilityVersion, searchCacheSchemaVersion } = require('../src/settings');
174176
const version = `${searchCacheSchemaVersion}-${searchCacheCompatibilityVersion}`;
177+
const cacheKey = `saved_uuid_search_schema_${version}`;
178+
const deleteCachedDatabase = jest.fn(() => Promise.resolve());
175179

176180
global.window = { indexedDB: {} };
177181
global.localStorage.getItem.mockReturnValue(`${global.build_uuid}-search-${version}`);
178182

179183
jest.doMock('../src/search-service.js', () => class {
184+
constructor() {
185+
this.db = { indexeddb: { delete: deleteCachedDatabase } };
186+
}
187+
180188
initializeAsync() {
181189
return Promise.reject(new Error('cached index is unreadable'));
182190
}
183191
});
184192

185193
require('../src/index');
186194
await new Promise(resolve => setImmediate(resolve));
195+
196+
return { cacheKey, deleteCachedDatabase };
187197
}
188198

189199
function eventsForSelector(selector) {

attack-search/src/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,23 @@ function markSearchUnavailable(reason) {
104104
searchButton.prop('title', reason);
105105
}
106106

107+
// Remove a failed cached index so the next page load rebuilds it instead of retrying
108+
// the same restore path. Cache cleanup is best-effort and must not mask the original
109+
// initialization failure or prevent the unavailable UI state from being shown.
110+
async function invalidateSearchCache() {
111+
try {
112+
localStorage.removeItem(searchCacheKey);
113+
} catch (error) {
114+
console.error('Failed to remove the search cache marker:', error);
115+
}
116+
117+
try {
118+
await searchService.db.indexeddb.delete();
119+
} catch (error) {
120+
console.error('Failed to delete the cached search index:', error);
121+
}
122+
}
123+
107124
const SEARCH_INDEX_FAILED_MESSAGE = 'The search index could not be built. Reload the page to try again.';
108125

109126
// Initialize the search service
@@ -132,6 +149,7 @@ async function initializeSearchService() {
132149
} catch (error) {
133150
console.error('Failed to initialize SearchService:', error);
134151
markSearchUnavailable(SEARCH_INDEX_FAILED_MESSAGE);
152+
await invalidateSearchCache();
135153
} finally {
136154
searchParsingIcon.hide();
137155
}

0 commit comments

Comments
 (0)