@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
164 lines (163 loc) • 5.04 kB
JavaScript
/**
* 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
};
}