fix(typst-preview): use persistent session to avoid Rc ownership panics
Build and Deploy Verso / deploy (push) Successful in 11m4s
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:
@@ -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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user