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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Website Changelog

## Unreleased

### Bug Fixes

* Settle the search index write when an IndexedDB write fails, instead of leaving the promise pending and the search spinner up.

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

* Release ATT&CK content version 19.2.
Expand Down
25 changes: 25 additions & 0 deletions attack-search/__tests__/indexed-db-wrapper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,29 @@ describe('IndexedDBWrapper', () => {
const count = await contentDb.count();
expect(count).toEqual(data.length);
});

// A failed write must settle the promise. Racing against a sentinel tells a
// rejection apart from a promise that never settles at all, which a plain
// rejects assertion cannot do: it would time out and look like a slow test.
const settle = (promise) => Promise.race([
promise.then(() => 'resolved', (error) => `rejected:${error.message}`),
new Promise((resolve) => setTimeout(() => resolve('HUNG'), 1000)),
]);

test('Bulk put rejects when the underlying write fails', async () => {
jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut')
.mockRejectedValue(new Error('QuotaExceededError'));

await expect(settle(contentDb.bulkPut(data))).resolves.toBe('rejected:QuotaExceededError');
});

test('Bulk put rejects when a later chunk fails', async () => {
let calls = 0;
jest.spyOn(contentDb.indexeddb[contentDb.tableName], 'bulkPut')
.mockImplementation(() => (++calls === 2
? Promise.reject(new Error('DatabaseClosedError'))
: Promise.resolve()));

await expect(settle(contentDb.bulkPut(data, 1))).resolves.toBe('rejected:DatabaseClosedError');
});
});
39 changes: 22 additions & 17 deletions attack-search/src/indexed-db-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class TableWrapper {
*/
async bulkPut(data, chunkSize = 100) {

return new Promise(async (resolve) => {
return new Promise((resolve, reject) => {
/**
* Schedules work using requestIdleCallback if supported, or setTimeout as a fallback.
* @param {Function} callback - The function to be executed when the browser is idle or after the specified delay.
Expand All @@ -44,23 +44,28 @@ class TableWrapper {
* @param {number} start - The index of the first item in the data array to be included in the current chunk.
*/
const putChunk = async (start) => {
// If all data has been processed, resolve the promise
if (start >= data.length) {
resolve();
return;
try {
// If all data has been processed, resolve the promise
if (start >= data.length) {
resolve();
return;
}

// Determine the end index for the current chunk
const end = Math.min(start + chunkSize, data.length);

// Extract the chunk from the data array
const chunk = data.slice(start, end);

// Insert the chunk into the IndexedDB table
await this.indexeddb[this.tableName].bulkPut(chunk);

// Schedule the next chunk to be processed
scheduleWork(() => putChunk(end));
} catch (error) {
// Nothing else settles this promise, so callers would wait forever.
reject(error);
}

// Determine the end index for the current chunk
const end = Math.min(start + chunkSize, data.length);

// Extract the chunk from the data array
const chunk = data.slice(start, end);

// Insert the chunk into the IndexedDB table
await this.indexeddb[this.tableName].bulkPut(chunk);

// Schedule the next chunk to be processed
scheduleWork(() => putChunk(end));
};

// Start processing the data array by inserting the first chunk
Expand Down