From f415551fb45174f2618f916fd323fb076adfd7a6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 2 May 2024 15:15:55 -0300 Subject: client: move search into worker, defer loading --- src/static/js/client.js | 149 +++++++++++++++++++++++++++++++++-------- src/static/js/search-worker.js | 92 +++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 27 deletions(-) create mode 100644 src/static/js/search-worker.js (limited to 'src/static') diff --git a/src/static/js/client.js b/src/static/js/client.js index 4836bedd..678e3444 100644 --- a/src/static/js/client.js +++ b/src/static/js/client.js @@ -12,12 +12,11 @@ import { atOffset, empty, filterMultipleArrays, + promiseWithResolvers, stitchArrays, withEntries, } from '../shared-util/sugar.js'; -import FlexSearch from '../lib/flexsearch/flexsearch.bundle.module.min.js'; - import {fetchWithProgress} from './xhr-util.js'; const clientInfo = window.hsmusicClientInfo = Object.create(null); @@ -3429,38 +3428,134 @@ clientSteps.addPageListeners.push(addArtistExternalLinkTooltipPageListeners); // Internal search functionality -------------------------- -async function initSearch() { - const indexes = - makeSearchIndexes(FlexSearch); +const wikiSearchInfo = initInfo('wikiSearchInfo', { + state: { + worker: null, - const searchData = - await fetch('/search-data/index.json') - .then(resp => resp.json()); + workerReadyPromise: null, + resolveWorkerReadyPromise: null, - // If this fails, it's because an outdated index was cached. - // TODO: If this fails, try again once with a cache busting url. - for (const [indexName, indexData] of Object.entries(searchData)) { - for (const [key, value] of Object.entries(indexData)) { - indexes[indexName].import(key, value); - } + workerActionCounter: 0, + workerActionPromiseMap: new Map(), + }, +}); + +async function initializeSearchWorker() { + const {state} = wikiSearchInfo; + + if (state.worker) { + return await state.workerReadyPromise; } - // Expose variable to window - window.searchIndexes = indexes; + state.worker = + new Worker( + import.meta.resolve('./search-worker.js'), + {type: 'module'}); + + state.worker.onmessage = handleSearchWorkerMessage; + + ({promise: state.workerReadyPromise, + resolve: state.resolveWorkerReadyPromise} = + promiseWithResolvers()); + + return await state.workerReadyPromise; } -function searchAll(query, options = {}) { - return ( - withEntries(window.searchIndexes, entries => entries - .map(([indexName, index]) => [ - indexName, - index.search(query, options), - ]))); +function handleSearchWorkerMessage(message) { + switch (message.data.kind) { + case 'status': + handleSearchWorkerStatusMessage(message); + break; + + case 'result': + handleSearchWorkerResultMessage(message); + break; + + default: + console.warn(`Unknown message kind "${message.data.kind}" <- from search worker`); + break; + } +} + +function handleSearchWorkerStatusMessage(message) { + const {state} = wikiSearchInfo; + + switch (message.data.status) { + case 'alive': + console.debug(`Search worker is alive, but not yet ready.`); + break; + + case 'ready': + console.debug(`Search worker has loaded corpuses and is ready.`); + state.resolveWorkerReadyPromise(state.worker); + break; + + default: + console.warn(`Unknown status "${message.data.status}" <- from search worker`); + break; + } +} + +function handleSearchWorkerResultMessage(message) { + const {state} = wikiSearchInfo; + const {id} = message.data; + + if (!id) { + console.warn(`Result without id <- from search worker:`, message.data); + return; + } + + if (!state.workerActionPromiseMap.has(id)) { + console.warn(`Runaway result id <- from search worker:`, message.data); + return; + } + + const {resolve, reject} = + state.workerActionPromiseMap.get(id); + + switch (message.data.status) { + case 'resolve': + resolve(message.data.value); + break; + + case 'reject': + reject(message.data.value); + break; + + default: + console.warn(`Unknown result status "${message.data.status}" <- from search worker`); + return; + } + + state.workerActionPromiseMap.delete(id); } -document.addEventListener('DOMContentLoaded', initSearch); +async function postSearchWorkerAction(action, options) { + const {state} = wikiSearchInfo; + + const worker = await initializeSearchWorker(); + const id = ++state.workerActionCounter; -window.searchAll = searchAll; + const {promise, resolve, reject} = promiseWithResolvers(); + + state.workerActionPromiseMap.set(id, {resolve, reject}); + + worker.postMessage({ + kind: 'action', + action: action, + id, + options, + }); + + return await promise; +} + +async function searchAll(query, options = {}) { + return await postSearchWorkerAction('search', { + query, + options, + }); +} // Sidebar search box ------------------------------------- @@ -3543,7 +3638,7 @@ function addSidebarSearchListeners() { }); } -function activateSidebarSearch(query) { +async function activateSidebarSearch(query) { const {state} = sidebarSearchInfo; if (state.stoppedTypingTimeout) { @@ -3551,7 +3646,7 @@ function activateSidebarSearch(query) { state.stoppedTypingTimeout = null; } - const results = searchAll(query, {enrich: true}); + const results = await searchAll(query, {enrich: true}); showSidebarSearchResults(results); } diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js new file mode 100644 index 00000000..7932655b --- /dev/null +++ b/src/static/js/search-worker.js @@ -0,0 +1,92 @@ +import {makeSearchIndexes} from '../shared-util/searchSchema.js'; +import {withEntries} from '../shared-util/sugar.js'; + +import FlexSearch from '../lib/flexsearch/flexsearch.bundle.module.min.js'; + +let status = null; + +onmessage = handleWindowMessage; + +postStatus('alive'); + +const indexes = + makeSearchIndexes(FlexSearch); + +const searchData = + await fetch('/search-data/index.json') + .then(resp => resp.json()); + +// If this fails, it's because an outdated index was cached. +// TODO: If this fails, try again once with a cache busting url. +for (const [indexName, indexData] of Object.entries(searchData)) { + for (const [key, value] of Object.entries(indexData)) { + indexes[indexName].import(key, value); + } +} + +postStatus('ready'); + +function handleWindowMessage(message) { + switch (message.data.kind) { + case 'action': + handleWindowActionMessage(message); + break; + + default: + console.warn(`Unknown message kind -> to search worker:`, message.data); + break; + } +} + +async function handleWindowActionMessage(message) { + const {id} = message.data; + + if (!id) { + console.warn(`Action without id -> to search worker:`, message.data); + return; + } + + if (status !== 'ready') { + return postActionResult(id, 'reject', 'not ready'); + } + + let value; + + switch (message.data.action) { + case 'search': + value = await performSearch(message.data.options); + break; + + default: + console.warn(`Unknown action "${message.data.action}" -> to search worker:`, message.data); + return postActionResult(id, 'reject', 'unknown action'); + } + + await postActionResult(id, 'resolve', value); +} + +function postStatus(newStatus) { + status = newStatus; + postMessage({ + kind: 'status', + status: newStatus, + }); +} + +function postActionResult(id, status, value) { + postMessage({ + kind: 'result', + id, + status, + value, + }); +} + +function performSearch({query, options}) { + return ( + withEntries(indexes, entries => entries + .map(([indexName, index]) => [ + indexName, + index.search(query, options), + ]))); +} -- cgit 1.3.0-6-gf8a5