« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/static/js')
-rw-r--r--src/static/js/client.js149
-rw-r--r--src/static/js/search-worker.js92
2 files changed, 214 insertions, 27 deletions
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),
+      ])));
+}