From c6d71e58b73b05fcc771ba6a457c941667bdac65 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 19 Jun 2026 14:52:49 +0000 Subject: [PATCH] fix(typst-preview): fix recursive Rust aliasing error and stale renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. 'recursive use of an object' Rust error: inside runWithSession(), calling renderer.renderToSvg({ renderSession: session }) passes the session to the renderer while runWithSession already holds it — double-aliasing the same Rust object. Fixed by using session.renderToSvg({ container }) directly. 2. Stale preview after edits: concurrent doRender calls (compile finishes while previous render is still in progress) would both enter runWithSession simultaneously, causing the Rust error and leaving the view frozen. Fixed with a render guard (isRenderingRef) that queues the latest vectorData and flushes it once the current render completes. Co-Authored-By: Claude Sonnet 4.6 --- .../components/typst-wasm-preview.tsx | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/services/web/frontend/js/features/typst-preview/components/typst-wasm-preview.tsx b/services/web/frontend/js/features/typst-preview/components/typst-wasm-preview.tsx index b3c0a5dada..aab70b3dc5 100644 --- a/services/web/frontend/js/features/typst-preview/components/typst-wasm-preview.tsx +++ b/services/web/frontend/js/features/typst-preview/components/typst-wasm-preview.tsx @@ -39,8 +39,12 @@ const TypstWasmPreview: FC = () => { const rendererRef = useRef(null) const isReadyRef = useRef(false) const compileTimerRef = useRef | null>(null) - // If 'compiled' arrives before renderer is ready, buffer the vector data + // If 'compiled' arrives before renderer is ready, buffer the latest vector data const pendingVectorRef = useRef(null) + // Prevent concurrent runWithSession calls which cause Rust aliasing errors + const isRenderingRef = useRef(false) + // Latest pending render while one is in progress + const renderQueueRef = useRef(null) const [status, setStatus] = useState('initializing') const [errorMsg, setErrorMsg] = useState('') @@ -52,17 +56,42 @@ const TypstWasmPreview: FC = () => { pendingVectorRef.current = vectorData return } - try { - await renderer.runWithSession(async session => { - session.manipulateData({ action: 'reset', data: vectorData }) - await renderer.renderToSvg({ renderSession: session, container }) - }) - pendingVectorRef.current = null - setStatus('ready') - setErrorMsg('') - } catch (e) { - setStatus('error') - setErrorMsg(`Render failed: ${e}`) + + // If a render is in progress, queue this data and return — + // the in-progress render will pick it up when it finishes + if (isRenderingRef.current) { + renderQueueRef.current = vectorData + return + } + + let dataToRender: Uint8Array | null = vectorData + while (dataToRender) { + isRenderingRef.current = true + const data = dataToRender + dataToRender = null + + try { + await renderer.runWithSession(async session => { + session.manipulateData({ action: 'reset', data }) + // Use session.renderToSvg (NOT renderer.renderToSvg with renderSession) + // to avoid double-aliasing the same Rust object inside runWithSession + await session.renderToSvg({ container }) + }) + pendingVectorRef.current = null + setStatus('ready') + setErrorMsg('') + } catch (e) { + setStatus('error') + setErrorMsg(`Render failed: ${e}`) + } finally { + isRenderingRef.current = false + } + + // Pick up any compile result that arrived while we were rendering + if (renderQueueRef.current) { + dataToRender = renderQueueRef.current + renderQueueRef.current = null + } } }, [])