From eda5379c176eaa253d780ea9bcaef3abab4cb9c3 Mon Sep 17 00:00:00 2001 From: Anurag Pappula <127645370+Anuragp22@users.noreply.github.com> Date: Fri, 29 May 2026 14:30:59 +0530 Subject: [PATCH] fix(docs/search): handle search index loading and failure states (#3184) * fix(docs/search): show loading state and replay query when index isn't ready Previously performSearch rendered "No results found" while the MiniSearch index was still loading, which is misleading on slow networks. Track the last query as pendingQuery and replay it from fetchSearchIndex once the index is ready; render a distinct "Failed to load search index" message on fetch error so the loading message doesn't persist indefinitely. Fixes #3180. * fix(docs/search): close stale-replay race and consolidate status messages Drop pendingQuery synchronously in the input handler when the query falls below the minimum length, so a fetchSearchIndex that resolves within the 300ms debounce window after the user clears the field can no longer replay the cleared query into an empty input. Move the idle, loading, and failed copy into a single renderSearchStatus(kind) helper so the four .search-no-results renderings can only drift in one place. --- src/docs/src/assets/js/search.js | 65 +++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/src/docs/src/assets/js/search.js b/src/docs/src/assets/js/search.js index 0e169d668..d239e4c90 100644 --- a/src/docs/src/assets/js/search.js +++ b/src/docs/src/assets/js/search.js @@ -5,6 +5,15 @@ let miniSearch = null; let searchTimeout = null; let selectedSearchResult = -1; +// Last query the user typed while the index was still loading; replayed by +// fetchSearchIndex() once the index becomes available so the user doesn't +// have to retype. Cleared on success, failure, or when the query falls +// below the minimum length. +let pendingQuery = null; +// True after fetchSearchIndex() has thrown at least once; lets performSearch +// distinguish "still loading" from "load failed" without waiting forever. +let indexLoadFailed = false; + // Query-time tokenizer. Splits on whitespace AND punctuation only — does // NOT emit the joined-without-punctuation form. The index (built in // build.js's indexTokenize) already pre-baked the joined variants, so @@ -119,11 +128,16 @@ $(document).ready(function () { clearTimeout(searchTimeout); } - // Set new timeout for debouncing - searchTimeout = setTimeout(async () => { - if ( !miniSearch ) { - await fetchSearchIndex(); - } + // Synchronously drop any pending replay when the input is too + // short, so a late index load can't resurrect stale results + // after the user clears the field within the debounce window. + if ( !query || query.length < 2 ) { + pendingQuery = null; + } + + // Set new timeout for debouncing. performSearch handles the + // not-yet-loaded case itself, so the input handler stays sync. + searchTimeout = setTimeout(() => { performSearch(query); }, 300); }); @@ -133,14 +147,26 @@ $(document).ready(function () { }); async function fetchSearchIndex () { + indexLoadFailed = false; try { const response = await fetch('/index.json'); const text = await response.text(); miniSearch = MiniSearch.loadJSON(text, MINISEARCH_CONFIG); console.log('Search index loaded:', `${miniSearch.documentCount } items`); + // Replay the query the user typed while we were still loading. + if ( pendingQuery !== null ) { + const q = pendingQuery; + pendingQuery = null; + performSearch(q); + } } catch ( error ) { console.error('Failed to load search index:', error); miniSearch = null; + indexLoadFailed = true; + if ( pendingQuery !== null ) { + pendingQuery = null; + renderSearchStatus('failed'); + } } } function escapeHtml (text) { @@ -177,16 +203,37 @@ function buildResultUrl (result) { return url; } +// Single source of truth for the empty-state UI so the idle / loading / +// failed copy can only drift in one place. +function renderSearchStatus (kind) { + const messages = { + idle: 'Start typing to search...', + loading: 'Loading search index...', + failed: 'Failed to load search index. Please refresh.', + }; + $('.search-results').html( + `
${messages[kind]}
` + ); +} + function performSearch (query) { if ( !query || query.length < 2 ) { - $('.search-results').html( - '
Start typing to search...
'); + // Drop any pending replay so the user isn't surprised by a stale + // search firing after they cleared the input. + pendingQuery = null; + renderSearchStatus('idle'); return; } if ( !miniSearch ) { - // Index hasn't finished loading yet; show empty state. - updateSearchResults([]); + if ( indexLoadFailed ) { + renderSearchStatus('failed'); + return; + } + // Index still loading; remember the query so fetchSearchIndex + // can replay it on success. + pendingQuery = query; + renderSearchStatus('loading'); return; }