UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

164 lines (163 loc) 5.04 kB
/** * Asynchronously parses source code into a HAST tree, typically off the main * thread (e.g. via a Web Worker). Used during live typing so the UI thread * stays responsive. Accepts an `AbortSignal` so a stale request can be * cancelled when newer keystrokes arrive. * * Internal: not exported as part of the public API. Consumers wire up async * parsing by passing a worker URL to `CodeProvider`; the produced client * exposes a `parseSourceAsync` callable directly without referring to this * type. */ /** * Create a worker-backed `parseSourceAsync` implementation. The caller owns * the lifecycle and must invoke `terminate()` on unmount. * * Each client owns exactly one underlying `Worker`. Concurrent in-flight * requests are demuxed by a monotonically increasing `id`. */ export function createParseSourceWorkerClient() { // Module workers are required (the worker uses `import` statements). On // browsers that expose `Worker` but reject `{ type: 'module' }` (older // Safari, some embedded webviews), the constructor throws synchronously. // Surface that as a tagged error so callers can fall back cleanly to the // main-thread highlighter instead of crashing the provider tree. let worker; try { worker = new Worker(new URL('./parseSourceWorker', import.meta.url), { type: 'module' }); } catch (cause) { throw new Error('Module workers are not supported in this environment', { cause }); } const pending = new Map(); let nextId = 1; let initPromise = null; worker.addEventListener('message', event => { const data = event.data; if (data.type === 'init-ack' || data.type === 'init-error' || data.type === 'register-ack' || data.type === 'register-error') { // `init()` / `register()` listen for these directly via their own // one-shot listeners. return; } if (data.type === 'parse') { const entry = pending.get(data.id); if (!entry) { // Aborted before response arrived — drop silently. return; } pending.delete(data.id); if (entry.signal && entry.onAbort) { entry.signal.removeEventListener('abort', entry.onAbort); } if (data.ok) { entry.resolve(data.hast); } else { entry.reject(new Error(data.error)); } } }); function init(grammars) { if (initPromise) { return initPromise; } initPromise = new Promise((resolve, reject) => { const onMessage = event => { if (event.data.type === 'init-ack') { worker.removeEventListener('message', onMessage); resolve(); } else if (event.data.type === 'init-error') { worker.removeEventListener('message', onMessage); reject(new Error(event.data.error)); } }; worker.addEventListener('message', onMessage); worker.postMessage({ type: 'init', grammars }); }); return initPromise; } function register(grammars) { return new Promise((resolve, reject) => { const onMessage = event => { if (event.data.type === 'register-ack') { worker.removeEventListener('message', onMessage); resolve(); } else if (event.data.type === 'register-error') { worker.removeEventListener('message', onMessage); reject(new Error(event.data.error)); } }; worker.addEventListener('message', onMessage); worker.postMessage({ type: 'register', grammars }); }); } const parseSourceAsync = async (source, fileName, language, signal) => { if (!initPromise) { throw new Error('parseSourceAsync called before init(). Call client.init(grammars) first.'); } if (signal?.aborted) { throw signal.reason; } await initPromise; if (signal?.aborted) { throw signal.reason; } const id = nextId; nextId += 1; return new Promise((resolve, reject) => { const onAbort = signal ? () => { const entry = pending.get(id); if (!entry) { return; } pending.delete(id); if (entry.signal && entry.onAbort) { entry.signal.removeEventListener('abort', entry.onAbort); } reject(signal.reason); } : undefined; pending.set(id, { resolve, reject, signal, onAbort }); if (signal && onAbort) { signal.addEventListener('abort', onAbort, { once: true }); } worker.postMessage({ type: 'parse', id, source, fileName, language }); }); }; function terminate() { worker.terminate(); for (const entry of pending.values()) { if (entry.signal && entry.onAbort) { entry.signal.removeEventListener('abort', entry.onAbort); } entry.reject(new Error('Worker terminated')); } pending.clear(); } return { init, register, parseSourceAsync, terminate }; }