# HG changeset patch # User MrJuneJune # Date 1785806616 25200 # Node ID 3843bb6253acfd1cac75f730e3f24ccbda4ac05f # Parent b8aa0850337832acc7edde23027f7cd1aea73cc1 [tools] Debounce live LaTeX preview Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff -r b8aa08503378 -r 3843bb6253ac mrjunejune/src/tools/latex_editor/index.js --- 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(); diff -r b8aa08503378 -r 3843bb6253ac mrjunejune/test/latex_editor_test.js --- a/mrjunejune/test/latex_editor_test.js Mon Aug 03 16:56:25 2026 -0700 +++ b/mrjunejune/test/latex_editor_test.js Mon Aug 03 18:23:36 2026 -0700 @@ -236,6 +236,25 @@ 'document.pdf', ); + let livePreviewRequests = 0; + const countLivePreviewRequest = request => { + if (request.url().endsWith('/api/latex/render')) livePreviewRequests++; + }; + page.on('request', countLivePreviewRequest); + const livePreviewResponse = page.waitForResponse(response => + response.url().endsWith('/api/latex/render') && + response.status() === 200 + ); + await page.locator('#latexSource').fill(valid.replace('API.', 'API change one.')); + await page.waitForTimeout(100); + await page.locator('#latexSource').fill(valid.replace('API.', 'API change two.')); + await page.waitForTimeout(100); + await page.locator('#latexSource').fill(valid.replace('API.', 'API final change.')); + await livePreviewResponse; + await page.locator('#latexStatus[data-state="ready"]').waitFor(); + page.off('request', countLivePreviewRequest); + assert.equal(livePreviewRequests, 1); + await page.locator('#autoCompile').uncheck(); await page.locator('#latexSource').fill( String.raw`\documentclass{article}\begin{document}\badcommand\end{document}`,