diff mrjunejune/src/tools/latex_editor/index.js @ 245:3843bb6253ac

[tools] Debounce live LaTeX preview Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 18:23:36 -0700
parents b8aa08503378
children
line wrap: on
line diff
--- a/mrjunejune/src/tools/latex_editor/index.js	Mon Aug 03 16:56:25 2026 -0700
+++ b/mrjunejune/src/tools/latex_editor/index.js	Mon Aug 03 18:23:36 2026 -0700
@@ -3,7 +3,7 @@
 
   const SOURCE_LIMIT = 64 * 1024;
   const STORAGE_KEY = "mrjunejune-latex-source";
-  const AUTO_COMPILE_DELAY = 700;
+  const AUTO_COMPILE_DELAY = 500;
 
   window.addEventListener("DOMContentLoaded", () => {
     const source = document.querySelector("#latexSource");
@@ -30,9 +30,10 @@
     if (savedSource) source.value = savedSource;
 
     let currentPdfUrl = null;
-    let compileTimer = null;
     let compileGeneration = 0;
     let controller = null;
+    let compilePending = false;
+    let retryDelay = 0;
 
     const byteLength = value => new TextEncoder().encode(value).byteLength;
 
@@ -77,12 +78,26 @@
       download.setAttribute("aria-disabled", "false");
     };
 
+    const createDebouncer = (callback, delay) => {
+      let timer = null;
+      const schedule = (nextDelay = delay) => {
+        clearTimeout(timer);
+        timer = setTimeout(() => {
+          timer = null;
+          callback();
+        }, nextDelay);
+      };
+      schedule.cancel = () => {
+        clearTimeout(timer);
+        timer = null;
+      };
+      return schedule;
+    };
+
+    let queueCompile;
     const compile = async () => {
-      clearTimeout(compileTimer);
+      queueCompile.cancel();
       const generation = ++compileGeneration;
-      if (controller) controller.abort();
-      controller = null;
-      compileButton.disabled = false;
       const text = source.value;
       const size = updateSize();
       if (!text.trim()) {
@@ -96,7 +111,15 @@
         return;
       }
 
-      controller = new AbortController();
+      if (controller) {
+        compilePending = true;
+        setStatus("Finishing the current PDF, then compiling your latest changes...", "working");
+        return;
+      }
+
+      compilePending = false;
+      const requestController = new AbortController();
+      controller = requestController;
       compileButton.disabled = true;
       setStatus("Compiling on the server...", "working");
 
@@ -108,11 +131,17 @@
           },
           body: text,
           cache: "no-store",
-          signal: controller.signal,
+          signal: requestController.signal,
         });
         if (generation !== compileGeneration) return;
 
         if (!response.ok) {
+          if (response.status === 429) {
+            compilePending = true;
+            retryDelay = 1000;
+            setStatus("The compiler is busy. Your latest changes are queued.", "working");
+            return;
+          }
           const message = await response.text();
           showDiagnostics(message || `Compilation failed (${response.status}).`);
           setStatus("Fix the LaTeX errors shown in the preview pane.", "error");
@@ -133,24 +162,26 @@
         showDiagnostics(error.message || "Unable to compile this document.");
         setStatus("The PDF request failed.", "error");
       } finally {
-        if (generation === compileGeneration) {
+        if (controller === requestController) {
+          controller = null;
           compileButton.disabled = false;
-          controller = null;
+        }
+        if (compilePending && !controller) {
+          compilePending = false;
+          const delay = retryDelay;
+          retryDelay = 0;
+          queueCompile(delay);
         }
       }
     };
 
-    const queueCompile = () => {
-      clearTimeout(compileTimer);
-      if (!autoCompile.checked) return;
-      compileTimer = setTimeout(compile, AUTO_COMPILE_DELAY);
-    };
+    queueCompile = createDebouncer(compile, AUTO_COMPILE_DELAY);
 
     const invalidateCompile = () => {
       compileGeneration++;
-      if (controller) controller.abort();
-      controller = null;
-      compileButton.disabled = false;
+      compilePending = false;
+      retryDelay = 0;
+      queueCompile.cancel();
     };
 
     source.addEventListener("input", () => {
@@ -171,7 +202,7 @@
         autoCompile.checked ? "Waiting to compile..." : "Changes ready to compile.",
         "working",
       );
-      queueCompile();
+      if (autoCompile.checked) queueCompile();
     });
     source.addEventListener("keydown", event => {
       if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
@@ -188,6 +219,10 @@
     });
     autoCompile.addEventListener("change", () => {
       if (autoCompile.checked) queueCompile();
+      else {
+        queueCompile.cancel();
+        compilePending = false;
+      }
     });
     window.addEventListener("beforeunload", () => {
       if (controller) controller.abort();