fix: cache typst compile result to eliminate race-condition failures
Build and Deploy Verso / deploy (push) Successful in 14m6s

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 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-08 11:48:46 +00:00
parent e0c717c131
commit 7e6c8c30cc
+20 -1
View File
@@ -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)
}