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'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { createTypstRenderer } from '@myriaddreamin/typst.ts'
|
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 rendererWasmUrl from '@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm'
|
||||||
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
|
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
|
||||||
import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context'
|
import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context'
|
||||||
@@ -37,13 +37,16 @@ const TypstWasmPreview: FC = () => {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const workerRef = useRef<Worker | null>(null)
|
const workerRef = useRef<Worker | null>(null)
|
||||||
const rendererRef = useRef<TypstRenderer | 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 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 latest 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
|
// Prevent concurrent renders
|
||||||
const isRenderingRef = useRef(false)
|
const isRenderingRef = useRef(false)
|
||||||
// Latest pending render while one is in progress
|
|
||||||
const renderQueueRef = useRef<Uint8Array | null>(null)
|
const renderQueueRef = useRef<Uint8Array | null>(null)
|
||||||
|
|
||||||
const [status, setStatus] = useState<Status>('initializing')
|
const [status, setStatus] = useState<Status>('initializing')
|
||||||
@@ -57,8 +60,6 @@ const TypstWasmPreview: FC = () => {
|
|||||||
return
|
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) {
|
if (isRenderingRef.current) {
|
||||||
renderQueueRef.current = vectorData
|
renderQueueRef.current = vectorData
|
||||||
return
|
return
|
||||||
@@ -71,13 +72,20 @@ const TypstWasmPreview: FC = () => {
|
|||||||
dataToRender = null
|
dataToRender = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use RenderByContentOptions: pass artifactContent directly instead of
|
let session = sessionRef.current
|
||||||
// using runWithSession + manipulateData, which causes Rust aliasing errors
|
if (!session) {
|
||||||
await renderer.renderToSvg({
|
// createModule creates a persistent session loaded with the vector data.
|
||||||
format: 'vector',
|
// It does NOT go through runWithSession, so there is no automatic free()
|
||||||
artifactContent: data,
|
// and no Rc::try_unwrap ownership conflict between renders.
|
||||||
container,
|
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
|
pendingVectorRef.current = null
|
||||||
setStatus('ready')
|
setStatus('ready')
|
||||||
setErrorMsg('')
|
setErrorMsg('')
|
||||||
@@ -88,7 +96,6 @@ const TypstWasmPreview: FC = () => {
|
|||||||
isRenderingRef.current = false
|
isRenderingRef.current = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick up any compile result that arrived while we were rendering
|
|
||||||
if (renderQueueRef.current) {
|
if (renderQueueRef.current) {
|
||||||
dataToRender = renderQueueRef.current
|
dataToRender = renderQueueRef.current
|
||||||
renderQueueRef.current = null
|
renderQueueRef.current = null
|
||||||
@@ -135,6 +142,8 @@ const TypstWasmPreview: FC = () => {
|
|||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
|
sessionRef.current = null
|
||||||
|
rendererRef.current = null
|
||||||
}
|
}
|
||||||
}, [doRender])
|
}, [doRender])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user