fix(typst-preview): fix recursive Rust aliasing error and stale renders
Build and Deploy Verso / deploy (push) Successful in 12m44s

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 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-19 14:52:49 +00:00
parent 06085cda21
commit c6d71e58b7
@@ -39,8 +39,12 @@ const TypstWasmPreview: FC = () => {
const rendererRef = useRef<TypstRenderer | null>(null) const rendererRef = useRef<TypstRenderer | null>(null)
const isReadyRef = useRef(false) const isReadyRef = useRef(false)
const compileTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) const compileTimerRef = useRef<ReturnType<typeof setTimeout> | 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<Uint8Array | null>(null) const pendingVectorRef = useRef<Uint8Array | null>(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<Uint8Array | null>(null)
const [status, setStatus] = useState<Status>('initializing') const [status, setStatus] = useState<Status>('initializing')
const [errorMsg, setErrorMsg] = useState('') const [errorMsg, setErrorMsg] = useState('')
@@ -52,17 +56,42 @@ const TypstWasmPreview: FC = () => {
pendingVectorRef.current = vectorData pendingVectorRef.current = vectorData
return return
} }
try {
await renderer.runWithSession(async session => { // If a render is in progress, queue this data and return —
session.manipulateData({ action: 'reset', data: vectorData }) // the in-progress render will pick it up when it finishes
await renderer.renderToSvg({ renderSession: session, container }) if (isRenderingRef.current) {
}) renderQueueRef.current = vectorData
pendingVectorRef.current = null return
setStatus('ready') }
setErrorMsg('')
} catch (e) { let dataToRender: Uint8Array | null = vectorData
setStatus('error') while (dataToRender) {
setErrorMsg(`Render failed: ${e}`) 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
}
} }
}, []) }, [])