fix(typst-preview): use persistent session to avoid Rc ownership panics
Build and Deploy Verso / deploy (push) Successful in 11m4s

Every call to renderToSvg({ artifactContent }) internally routes through
runWithSession, which calls session.free() after fn resolves. Because the JS
GC may still hold a reference to the first RenderSession wrapper, the next
render's Rc::try_unwrap() panics with 'attempted to take ownership of Rust
value while it was borrowed'.

Fix: use renderer.createModule(data) once to create a persistent RenderSession
that is never freed during the component's lifetime. Subsequent renders call
session.manipulateData({ action: 'reset', data }) (synchronous, no ownership
transfer) + session.renderToSvg({ container }) which routes through
withinOptionSession's renderSession fast-path — bypassing runWithSession and
its session.free() entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-19 15:49:50 +00:00
parent eedf4b50f6
commit 4f5dad383b
@@ -8,7 +8,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { createTypstRenderer } from '@myriaddreamin/typst.ts'
import type { TypstRenderer } from '@myriaddreamin/typst.ts'
import type { TypstRenderer, RenderSession } from '@myriaddreamin/typst.ts'
import rendererWasmUrl from '@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm'
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context'
@@ -37,13 +37,16 @@ const TypstWasmPreview: FC = () => {
const containerRef = useRef<HTMLDivElement>(null)
const workerRef = useRef<Worker | null>(null)
const rendererRef = useRef<TypstRenderer | null>(null)
// Persistent render session — created once, reused for all subsequent renders
// to avoid the Rust borrow/ownership panics that occur when runWithSession
// creates and frees a session on every render
const sessionRef = useRef<RenderSession | null>(null)
const isReadyRef = useRef(false)
const compileTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// If 'compiled' arrives before renderer is ready, buffer the latest vector data
const pendingVectorRef = useRef<Uint8Array | null>(null)
// Prevent concurrent runWithSession calls which cause Rust aliasing errors
// Prevent concurrent renders
const isRenderingRef = useRef(false)
// Latest pending render while one is in progress
const renderQueueRef = useRef<Uint8Array | null>(null)
const [status, setStatus] = useState<Status>('initializing')
@@ -57,8 +60,6 @@ const TypstWasmPreview: FC = () => {
return
}
// 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
@@ -71,13 +72,20 @@ const TypstWasmPreview: FC = () => {
dataToRender = null
try {
// Use RenderByContentOptions: pass artifactContent directly instead of
// using runWithSession + manipulateData, which causes Rust aliasing errors
await renderer.renderToSvg({
format: 'vector',
artifactContent: data,
container,
})
let session = sessionRef.current
if (!session) {
// createModule creates a persistent session loaded with the vector data.
// It does NOT go through runWithSession, so there is no automatic free()
// and no Rc::try_unwrap ownership conflict between renders.
session = await (renderer as any).createModule(data) as RenderSession
sessionRef.current = session
} else {
// Reset existing session with new vector data — synchronous, no ownership transfer
session.manipulateData({ action: 'reset', data })
}
// renderToSvg with an existing renderSession bypasses runWithSession entirely
// (withinOptionSession short-circuits to fn(session) when renderSession is present)
await session.renderToSvg({ container })
pendingVectorRef.current = null
setStatus('ready')
setErrorMsg('')
@@ -88,7 +96,6 @@ const TypstWasmPreview: FC = () => {
isRenderingRef.current = false
}
// Pick up any compile result that arrived while we were rendering
if (renderQueueRef.current) {
dataToRender = renderQueueRef.current
renderQueueRef.current = null
@@ -135,6 +142,8 @@ const TypstWasmPreview: FC = () => {
})
return () => {
cancelled = true
sessionRef.current = null
rendererRef.current = null
}
}, [doRender])