« get me outta code hell

client: sidebar-search: bound session storage, resilient time travel - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2026-03-03 17:50:54 -0400
committer(quasar) nebula <qznebula@protonmail.com>2026-03-03 17:52:23 -0400
commit8c2360a714e39651165d591124cf77a772f26fce (patch)
tree87d08edc854056dbbe1fa9988505cade04808ee8
parent488d0a1542e16b2552fbf7d477ad7d6eb753f8a6 (diff)
client: sidebar-search: bound session storage, resilient time travel preview
This is essentially the best we can get without taking advantage
of literally brand new navigation APIs, if we don't want to get
reeeeeeeeeeally futzy.

This approach is strictly oriented around the back/forward cache
and does not store anything in history state (replaceState etc).
-rw-r--r--src/static/js/client/index.js85
-rw-r--r--src/static/js/client/sidebar-search.js17
2 files changed, 97 insertions, 5 deletions
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
index 53432a91..16ebe89f 100644
--- a/src/static/js/client/index.js
+++ b/src/static/js/client/index.js
@@ -75,6 +75,10 @@ const situationalSteps = {
 
 const stepInfoSymbol = Symbol();
 
+const boundSessionStorage =
+  window.hsmusicBoundSessionStorage =
+  Object.create(null);
+
 for (const module of modules) {
   const {info} = module;
 
@@ -159,12 +163,47 @@ for (const module of modules) {
 
       const storageKey = `hsmusic.${infoKey}.${key}`;
 
+      // There are two storage systems besides actual session storage in play.
+      //
+      // "Fallback" is for if session storage is not available, which may
+      // suddenly become the case, i.e. access is temporarily revoked or fails.
+      // The fallback value is controlled completely internally i.e. in this
+      // infrastructure, in this lexical scope.
+      //
+      // "Bound" is for if the value kept in session storage was saved to
+      // the page when the page was initially loaded, rather than a living
+      // window on session storage (which may be affected by pages later in
+      // the history stack). Whether or not bound storage is in effect is
+      // controlled at page load (of course), by each module's own logic.
+      //
+      // Asterisk: Bound storage can't work miracles and if the page is
+      // actually deloaded with its JavaScript state discarded, the bound
+      // values are lost, even if the browser recreates on-page form state.
+
       let fallbackValue = defaultValue;
+      let boundValue = undefined;
+
+      const updateBoundValue = (givenValue = undefined) => {
+        if (givenValue) {
+          if (
+            infoKey in boundSessionStorage &&
+            key in boundSessionStorage[infoKey]
+          ) {
+            boundSessionStorage[infoKey][key] = givenValue;
+          }
+        } else {
+          boundValue = boundSessionStorage[infoKey]?.[key];
+        }
+      };
 
       Object.defineProperty(info.session, key, {
         get: () => {
+          updateBoundValue();
+
           let value;
-          try {
+          if (boundValue !== undefined) {
+            value = boundValue ?? defaultValue;
+          } else try {
             value = sessionStorage.getItem(storageKey) ?? defaultValue;
           } catch (error) {
             if (error instanceof DOMException) {
@@ -200,21 +239,23 @@ for (const module of modules) {
             return;
           }
 
-          let operation;
+          let sessionOperation;
           if (value === '') {
             fallbackValue = null;
-            operation = () => {
+            updateBoundValue(null);
+            sessionOperation = () => {
               sessionStorage.removeItem(storageKey);
             };
           } else {
             fallbackValue = value;
-            operation = () => {
+            updateBoundValue(value);
+            sessionOperation = () => {
               sessionStorage.setItem(storageKey, value);
             };
           }
 
           try {
-            operation();
+            sessionOperation();
           } catch (error) {
             if (!(error instanceof DOMException)) {
               throw error;
@@ -244,6 +285,40 @@ for (const module of modules) {
   }
 }
 
+function evaluateBindSessionStorageStep(bindSessionStorage) {
+  const {id: infoKey, session: moduleExposedSessionObject} =
+    bindSessionStorage[stepInfoSymbol];
+
+  const generator = bindSessionStorage();
+
+  let lastBoundValue;
+  while (true) {
+    const {value: key, done} = generator.next(lastBoundValue);
+    const storageKey = `hsmusic.${infoKey}.${key}`;
+
+    let value = undefined;
+    try {
+      value = sessionStorage.getItem(storageKey);
+    } catch (error) {
+      if (!(error instanceof DOMException)) {
+        throw error;
+      }
+    }
+
+    if (value === undefined) {
+      // This effectively gets the default value.
+      value = moduleExposedSessionObject[key];
+    }
+
+    boundSessionStorage[infoKey] ??= Object.create(null);
+    boundSessionStorage[infoKey][key] = value;
+
+    lastBoundValue = value;
+
+    if (done) break;
+  }
+}
+
 function evaluateStep(stepsObject, key) {
   for (const step of stepsObject[key]) {
     try {
diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js
index 3cc3d218..0b905948 100644
--- a/src/static/js/client/sidebar-search.js
+++ b/src/static/js/client/sidebar-search.js
@@ -158,6 +158,17 @@ export const info = {
   },
 };
 
+export function* bindSessionStorage() {
+  if (yield 'activeQuery') {
+    yield 'activeQueryContextPageName';
+    yield 'activeQueryContextPagePathname';
+    yield 'activeQueryContextPageColor';
+    yield 'activeQueryResults';
+    yield 'activeFilterType';
+    yield 'resultsScrollOffset';
+  }
+}
+
 export function getPageReferences() {
   info.pageContainer =
     document.getElementById('page-container');
@@ -443,6 +454,12 @@ export function mutatePageContent() {
 
   info.searchBox.appendChild(info.endSearchRule);
   info.searchBox.appendChild(info.endSearchLine);
+
+  // Accommodate the web browser reconstructing the search input with a value
+  // that was previously entered (or restored after recall), i.e. because
+  // the user is traversing very far back in history and yet the browser is
+  // trying to rebuild the page as-was anyway, by telling it "no don't".
+  info.searchInput.value = '';
 }
 
 export function addPageListeners() {