From 7e6c8c30ccc1aac466f8b0bb0aa3c7ff7432e7c6 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 8 Jun 2026 11:48:46 +0000 Subject: [PATCH] fix: cache typst compile result to eliminate race-condition failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When typst watch detects a file change and compiles before the CLSI resolver is registered (ResourceWriter writes files → typst compiles → runTypst is called), _resolveAllPending was discarding the result because pendingResolvers was empty. This caused two symptoms: 1. output.log only contained "compiled with errors" (no diagnostics) because the result carrying the full stdout was thrown away. 2. Every other manual compile failed with "compilation already gone" because the missed result caused a timeout, which killed the watcher and triggered a watcher restart cycle (success → miss → timeout → kill → restart → success → miss → ...). Fix: when _resolveAllPending fires with no pending resolvers, store the result in entry.pendingResult. _waitForNextCompile checks this field first and resolves immediately if a cached result is present. Co-Authored-By: Claude Sonnet 4.6 --- services/clsi/app/js/TypstRunner.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/services/clsi/app/js/TypstRunner.js b/services/clsi/app/js/TypstRunner.js index e87d99ce01..2193309621 100644 --- a/services/clsi/app/js/TypstRunner.js +++ b/services/clsi/app/js/TypstRunner.js @@ -55,6 +55,8 @@ const ProcessTable = {} // accumulator incomplete trailing line from the last data chunk // currentLines lines accumulated since the last compile-done event // pendingResolvers Array<{resolve, reject, timeoutHandle}> +// pendingResult compile result cached when typst finished before a +// resolver was registered (race-condition safety net) const WatchTable = {} // PIDs we have intentionally killed, so the close handler can distinguish @@ -141,6 +143,7 @@ async function _startWatcher(compileName, options) { accumulator: '', currentLines: [], pendingResolvers: [], + pendingResult: null, } WatchTable[compileName] = entry @@ -255,6 +258,15 @@ function _waitForNextCompile(compileName, timeout) { return reject(new Error('no typst watcher for ' + compileName)) } + // If typst finished a compile cycle before this resolver was registered + // (race: ResourceWriter wrote files → typst compiled → runTypst called), + // consume the cached result immediately instead of waiting for a timeout. + if (entry.pendingResult) { + const result = entry.pendingResult + entry.pendingResult = null + return resolve(result) + } + // Push synchronously inside the Promise constructor — before the first // await in the caller, so no data event can fire in the gap. const timeoutHandle = setTimeout(() => { @@ -272,7 +284,14 @@ function _waitForNextCompile(compileName, timeout) { function _resolveAllPending(compileName, result) { const entry = WatchTable[compileName] if (!entry) return - for (const { resolve, timeoutHandle } of entry.pendingResolvers.splice(0)) { + const resolvers = entry.pendingResolvers.splice(0) + if (resolvers.length === 0) { + // typst compiled before a resolver was registered — cache the result so + // the next _waitForNextCompile call can consume it immediately. + entry.pendingResult = result + return + } + for (const { resolve, timeoutHandle } of resolvers) { clearTimeout(timeoutHandle) resolve(result) }