@@ -5,6 +5,10 @@ interface Pending {
55}
66
77let worker : Worker | null = null
8+ // runtime debug flag controlled by the main thread
9+ let DEBUG_KATEX_WORKER = false
10+ // telemetry / diagnostics listeners for worker errors/timeouts
11+ const errorListeners = new Set < ( err : any ) => void > ( )
812const pending = new Map < string , Pending > ( )
913// Simple in-memory cache to avoid repeated renders for identical input.
1014const cache = new Map < string , string > ( )
@@ -17,27 +21,53 @@ function ensureWorker() {
1721 // Only create a Worker in a browser environment
1822 if ( typeof window === 'undefined' ) {
1923 worker = null
24+ try {
25+ console . warn ( '[katexWorkerClient] window is undefined — Web Worker will not be created' )
26+ }
27+ catch { }
2028 }
2129 else {
2230 // Vite-friendly worker instantiation. Bundlers will inline the worker when configured.
2331 worker = new Worker ( new URL ( './katexRenderer.worker.ts' , import . meta. url ) , { type : 'module' } )
32+ try {
33+ // send initial debug flag to worker so it can gate debug logs
34+ ; ( worker as any ) . postMessage ( { type : 'init' , debug : DEBUG_KATEX_WORKER } )
35+ }
36+ catch { }
2437 }
2538 }
2639 catch {
2740 worker = null
41+ try {
42+ console . warn ( '[katexWorkerClient] failed to instantiate Web Worker' )
43+ }
44+ catch { }
2845 }
2946
3047 if ( worker ) {
3148 worker . addEventListener ( 'message' , ( ev : MessageEvent ) => {
3249 const { id, html, error, content, displayMode } = ev . data as any
3350 const p = pending . get ( id )
3451 if ( ! p ) {
52+ try {
53+ console . warn ( '[katexWorkerClient] received message for unknown id' , id , { content, displayMode, error } )
54+ }
55+ catch { }
3556 return
3657 }
3758 ( globalThis as any ) . clearTimeout ( p . timeoutId )
3859 pending . delete ( id )
3960 if ( error ) {
40- p . reject ( new Error ( error ) )
61+ const err = new Error ( String ( error ) )
62+ ; ( err as any ) . name = 'WorkerRenderError'
63+ ; ( err as any ) . code = 'WORKER_RENDER_ERROR'
64+ ; ( err as any ) . content = content
65+ ; ( err as any ) . displayMode = displayMode
66+ try {
67+ console . warn ( '[katexWorkerClient] worker returned an error for id' , id , String ( error ) )
68+ }
69+ catch { }
70+ p . reject ( err )
4171 return
4272 }
4373
@@ -51,17 +81,79 @@ function ensureWorker() {
5181 cache . delete ( firstKey )
5282 }
5383 }
54- catch {
55- // ignore cache errors
84+ catch ( e ) {
85+ try {
86+ console . warn ( '[katexWorkerClient] cache set failed' , e )
87+ }
88+ catch { }
5689 }
5790
5891 p . resolve ( html )
5992 } )
93+
94+ worker . addEventListener ( 'error' , ( ev ) => {
95+ try {
96+ console . error ( '[katexWorkerClient] Worker error' , ev )
97+ }
98+ catch { }
99+ // reject all pending promises so callers can fallback
100+ for ( const [ _id , p ] of pending . entries ( ) ) {
101+ try {
102+ const err = new Error ( 'Worker crashed' )
103+ ; ( err as any ) . name = 'WorkerCrashed'
104+ ; ( err as any ) . code = 'WORKER_CRASHED'
105+ try {
106+ for ( const h of errorListeners ) {
107+ try {
108+ h ( err )
109+ }
110+ catch { }
111+ }
112+ }
113+ catch { }
114+ p . reject ( err )
115+ }
116+ catch { }
117+ }
118+ pending . clear ( )
119+ } )
120+
121+ worker . addEventListener ( 'messageerror' , ( ev ) => {
122+ try {
123+ console . error ( '[katexWorkerClient] Worker messageerror' , ev )
124+ }
125+ catch { }
126+ } )
60127 }
61128
62129 return worker
63130}
64131
132+ // Allow toggling verbose worker debug logs at runtime. When set, we post an init
133+ // message to an existing worker so the worker can enable logs.
134+ export function setKaTeXWorkerDebug ( enabled : boolean ) {
135+ DEBUG_KATEX_WORKER = ! ! enabled
136+ if ( worker ) {
137+ try {
138+ ; ( worker as any ) . postMessage ( { type : 'init' , debug : DEBUG_KATEX_WORKER } )
139+ }
140+ catch {
141+ try {
142+ console . warn ( '[katexWorkerClient] failed to send debug init to worker' )
143+ }
144+ catch { }
145+ }
146+ }
147+ }
148+
149+ export function onKaTeXWorkerError ( fn : ( err : any ) => void ) {
150+ errorListeners . add ( fn )
151+ }
152+
153+ export function offKaTeXWorkerError ( fn : ( err : any ) => void ) {
154+ errorListeners . delete ( fn )
155+ }
156+
65157export async function renderKaTeXInWorker ( content : string , displayMode = true , timeout = 2000 , signal ?: AbortSignal ) : Promise < string > {
66158 // Quick cache hit
67159 const cacheKey = `${ displayMode ? 'd' : 'i' } :${ content } `
@@ -84,7 +176,19 @@ export async function renderKaTeXInWorker(content: string, displayMode = true, t
84176 const id = Math . random ( ) . toString ( 36 ) . slice ( 2 )
85177 const timeoutId = ( globalThis as any ) . setTimeout ( ( ) => {
86178 pending . delete ( id )
87- reject ( new Error ( 'Worker render timed out' ) )
179+ const err = new Error ( 'Worker render timed out' )
180+ ; ( err as any ) . name = 'WorkerTimeout'
181+ ; ( err as any ) . code = 'WORKER_TIMEOUT'
182+ try {
183+ for ( const h of errorListeners ) {
184+ try {
185+ h ( err )
186+ }
187+ catch { }
188+ }
189+ }
190+ catch { }
191+ reject ( err )
88192 } , timeout )
89193
90194 // Listen for abort to cancel this pending request
@@ -105,6 +209,22 @@ export async function renderKaTeXInWorker(content: string, displayMode = true, t
105209 } )
106210}
107211
212+ // Allow callers (e.g. main-thread fallback renderers) to populate the internal cache
213+ // so that synchronous renders can benefit subsequent worker-based calls.
214+ export function setKaTeXCache ( content : string , displayMode = true , html : string ) {
215+ try {
216+ const cacheKey = `${ displayMode ? 'd' : 'i' } :${ content } `
217+ cache . set ( cacheKey , html )
218+ if ( cache . size > CACHE_MAX ) {
219+ const firstKey = cache . keys ( ) . next ( ) . value
220+ cache . delete ( firstKey )
221+ }
222+ }
223+ catch {
224+ // ignore cache errors
225+ }
226+ }
227+
108228// When a worker response arrives we set the cache (handled in ensureWorker message handler),
109229// but the handler does not currently set cache; to keep cache coherent, also set here by
110230// wrapping the Promise resolution above would be ideal. However, pending resolution occurs
0 commit comments