diff --git a/attack-search/__tests__/search-service.test.js b/attack-search/__tests__/search-service.test.js index 32e2a3b3a19..a6ade5fff0a 100644 --- a/attack-search/__tests__/search-service.test.js +++ b/attack-search/__tests__/search-service.test.js @@ -52,6 +52,36 @@ describe('SearchService', () => { expect(tableResults).toEqual(data); }); + test('Backup search index completes when FlexSearch exports fewer than nine persisted chunks', async () => { + const exportedChunks = [ + ['title.1.map', 'title map data'], + ['content.1.map', 'content map data'], + ['content.1.ctx', 'content context data'], + ['1.reg', 'register data'], + ]; + + searchService.attackIndex = { + index: { + export: jest.fn((handler) => { + exportedChunks.forEach(([key, value], index) => { + setTimeout(() => handler(key, value), index); + }); + }), + }, + }; + searchService.searchIndexDb = { + put: jest.fn(() => Promise.resolve()), + }; + + const result = await Promise.race([ + searchService.backupSearchIndex().then(() => 'completed'), + new Promise((resolve) => setTimeout(() => resolve('timed out'), 250)), + ]); + + expect(result).toBe('completed'); + expect(searchService.searchIndexDb.put).toHaveBeenCalledTimes(exportedChunks.length); + }); + test('Resolve search results', async () => { await searchService.initializeAsync(data); diff --git a/attack-search/src/search-service.js b/attack-search/src/search-service.js index d5d010e755b..627ea58ff8a 100644 --- a/attack-search/src/search-service.js +++ b/attack-search/src/search-service.js @@ -190,25 +190,46 @@ module.exports = class SearchService { * @returns {Promise>} */ async backupSearchIndex() { + return new Promise((resolve, reject) => { + let idleTimer = null; + let pendingWrites = 0; + let settled = false; + + const resolveWhenIdle = () => { + clearTimeout(idleTimer); + idleTimer = setTimeout(() => { + if (!settled && pendingWrites === 0) { + settled = true; + resolve(true); + } + }, 100); + }; - const keys = []; - let processedKeys = 0; - - // totalKeys(x) = (3 * #searchFields) + 3 - // ^ - // title + content --> 2 fields - const totalKeys = 9; + const rejectOnce = (error) => { + if (settled) return; + settled = true; + clearTimeout(idleTimer); + reject(error); + }; - return new Promise((resolve) => { - this.attackIndex.index.export(async (key, data) => { - await this.searchIndexDb.put({ key, data }); - keys.push(key); - processedKeys++; + this.attackIndex.index.export((key, data) => { + clearTimeout(idleTimer); - if (processedKeys === totalKeys) { - resolve(true); + if (data === undefined) { + resolveWhenIdle(); + return null; } + + pendingWrites++; + return this.searchIndexDb.put({ key, data }) + .catch(rejectOnce) + .finally(() => { + pendingWrites--; + resolveWhenIdle(); + }); }); + + resolveWhenIdle(); }); }