Skip to content

Commit bc7ae63

Browse files
committed
fix: improve data saving logic in search.js
1 parent 4c19380 commit bc7ae63

2 files changed

Lines changed: 160 additions & 9 deletions

File tree

src/plugins/search/search.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,29 @@ db.version(1).stores({
1616
});
1717

1818
async function saveData(maxAge, expireKey) {
19-
INDEXES = Object.values(INDEXES).flatMap(innerData =>
20-
Object.values(innerData),
21-
);
22-
await /** @type {any} */ (db).search.bulkPut(INDEXES);
19+
const records = [];
20+
21+
Object.values(INDEXES).forEach(entry => {
22+
if (!entry || typeof entry !== 'object') {
23+
return;
24+
}
25+
26+
// Entry may already be a flat record read from IndexedDB.
27+
if ('slug' in entry) {
28+
records.push(entry);
29+
return;
30+
}
31+
32+
// Entry may be a per-path map of slug -> record produced by genIndex().
33+
Object.values(entry).forEach(item => {
34+
if (item && typeof item === 'object' && 'slug' in item) {
35+
records.push(item);
36+
}
37+
});
38+
});
39+
40+
INDEXES = records;
41+
await /** @type {any} */ (db).search.bulkPut(records);
2342
await /** @type {any} */ (db).expires.put({
2443
key: expireKey,
2544
value: Date.now() + maxAge,
@@ -306,26 +325,34 @@ export async function init(config, vm) {
306325
const len = paths.length;
307326
let count = 0;
308327

328+
const markComplete = async () => {
329+
if (len === ++count) {
330+
await saveData(config.maxAge, expireKey);
331+
}
332+
};
333+
309334
paths.forEach(path => {
310335
const pathExists = Array.isArray(INDEXES)
311336
? INDEXES.some(obj => obj.path === path)
312337
: false;
313338
if (pathExists) {
314-
return count++;
339+
void markComplete();
340+
return;
315341
}
316342

317343
Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then(
318-
async result => {
344+
result => {
319345
INDEXES[path] = genIndex(
320346
path,
321347
result,
322348
vm.router,
323349
config.depth,
324350
indexKey,
325351
);
326-
if (len === ++count) {
327-
await saveData(config.maxAge, expireKey);
328-
}
352+
return markComplete();
353+
},
354+
() => {
355+
return markComplete();
329356
},
330357
);
331358
});

test/e2e/search.test.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,130 @@ test.describe('Search Plugin Tests', () => {
198198
await expect(resultsHeadingElm).toHaveText('EmptyContent');
199199
});
200200

201+
test('keeps saving index when one auto path request fails with cached records', async ({
202+
page,
203+
}) => {
204+
const indexKey = 'docsify.search.index';
205+
const expireKey = 'docsify.search.expires';
206+
207+
const pageErrors = [];
208+
page.on('pageerror', error => pageErrors.push(error.message));
209+
210+
await page.evaluate(
211+
({ indexKey, expireKey }) => {
212+
return new Promise((resolve, reject) => {
213+
const request = indexedDB.open('docsify', 1);
214+
215+
request.onupgradeneeded = () => {
216+
const db = request.result;
217+
218+
if (!db.objectStoreNames.contains('search')) {
219+
db.createObjectStore('search', { keyPath: 'slug' });
220+
}
221+
222+
if (!db.objectStoreNames.contains('expires')) {
223+
db.createObjectStore('expires', { keyPath: 'key' });
224+
}
225+
};
226+
227+
request.onerror = () => reject(request.error);
228+
request.onsuccess = () => {
229+
const db = request.result;
230+
const tx = db.transaction(['search', 'expires'], 'readwrite');
231+
232+
tx.objectStore('search').put({
233+
slug: '/cached',
234+
title: 'Cached Page',
235+
body: 'cached record',
236+
path: '/cached',
237+
indexKey,
238+
});
239+
tx.objectStore('expires').put({
240+
key: expireKey,
241+
value: Date.now() + 60 * 1000,
242+
});
243+
244+
tx.oncomplete = () => {
245+
db.close();
246+
resolve();
247+
};
248+
tx.onerror = () => reject(tx.error);
249+
};
250+
});
251+
},
252+
{ indexKey, expireKey },
253+
);
254+
255+
await docsifyInit({
256+
markdown: {
257+
homepage: '# Home',
258+
sidebar: `
259+
- [Cached](cached)
260+
- [Success](success)
261+
- [Fail](fail)
262+
`,
263+
},
264+
routes: {
265+
'/success.md': '# Success\n\nregressionKeyword',
266+
'/fail.md': {
267+
status: 404,
268+
body: 'Not Found',
269+
contentType: 'text/markdown',
270+
},
271+
},
272+
scriptURLs: ['/dist/plugins/search.js'],
273+
});
274+
275+
await expect
276+
.poll(async () => {
277+
return await page.evaluate(indexKey => {
278+
return new Promise((resolve, reject) => {
279+
const request = indexedDB.open('docsify');
280+
281+
request.onerror = () => reject(request.error);
282+
request.onsuccess = () => {
283+
const db = request.result;
284+
const tx = db.transaction(['search', 'expires'], 'readonly');
285+
const searchStore = tx.objectStore('search');
286+
const expiresStore = tx.objectStore('expires');
287+
const searchReq = searchStore.getAll();
288+
const expiresReq = expiresStore.get('docsify.search.expires');
289+
290+
tx.onerror = () => reject(tx.error);
291+
tx.oncomplete = () => {
292+
const records = Array.isArray(searchReq.result)
293+
? searchReq.result
294+
: [];
295+
const hasSuccessRecord = records.some(
296+
record =>
297+
record &&
298+
record.indexKey === indexKey &&
299+
record.path === '/success',
300+
);
301+
const hasInvalidRecord = records.some(
302+
record => !record || typeof record.slug !== 'string',
303+
);
304+
const hasExpireRecord = Boolean(expiresReq.result?.value);
305+
306+
db.close();
307+
resolve(
308+
hasSuccessRecord && hasExpireRecord && !hasInvalidRecord,
309+
);
310+
};
311+
};
312+
});
313+
}, indexKey);
314+
})
315+
.toBe(true);
316+
317+
const searchFieldElm = page.locator('input[type=search]');
318+
const resultsHeadingElm = page.locator('.results-panel .title');
319+
320+
await searchFieldElm.fill('regressionKeyword');
321+
await expect(resultsHeadingElm).toHaveText('Success');
322+
expect(pageErrors).toEqual([]);
323+
});
324+
201325
test('handles default focusSearch binding', async ({ page }) => {
202326
const docsifyInitConfig = {
203327
scriptURLs: ['/dist/plugins/search.js'],

0 commit comments

Comments
 (0)