From 4f5dad383bb14ba63b938e73cc11638d00e17509 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 19 Jun 2026 15:49:50 +0000 Subject: [PATCH] fix(typst-preview): use persistent session to avoid Rc ownership panics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/typst-wasm-preview.tsx | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 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 4d830e27b7..118abb8c6a 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 @@ -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(null) const workerRef = useRef(null) const rendererRef = useRef(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(null) const isReadyRef = useRef(false) const compileTimerRef = useRef | null>(null) // 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 + // Prevent concurrent renders const isRenderingRef = useRef(false) - // Latest pending render while one is in progress const renderQueueRef = useRef(null) const [status, setStatus] = useState('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])