import MarkdownWorkerUrl from "./markdown.worker.ts?worker&url" import { applyMarkdownWorkerResponse, shouldReleaseMarkdownWorkerState, type MarkdownWorkerRequest, type MarkdownWorkerResponse, type MarkdownWorkerState, } from "./markdown-worker-protocol" import { createWorkerTransport } from "./markdown-worker-transport" import type { Projection } from "./markdown-stream" type HighlightPending = { key: string complete: boolean resolve: (state: MarkdownWorkerState) => void reject: (error: Error) => void } type ProjectPending = { key: string resolve: (projection: Projection) => void reject: (error: Error) => void } type ParsePending = { resolve: (html: string) => void reject: (error: Error) => void } let worker: Worker | undefined let disabled: Error | undefined let nextID = 0 const pending = new Map() const projects = new Map() const parses = new Map() const states = new Map() const keys = new Set() const latest = new Map() const transport = createWorkerTransport>({ post: (request) => worker!.postMessage(request), supersede: (request) => { const result = pending.get(request.id) if (!result) return pending.delete(request.id) result.reject(new MarkdownWorkerSupersededError()) }, }) const projectTransport = createWorkerTransport>({ post: (request) => worker!.postMessage(request), supersede: (request) => { const result = projects.get(request.id) if (!result) return projects.delete(request.id) result.reject(new MarkdownWorkerSupersededError()) }, }) export function parseMarkdown(text: string) { const instance = getWorker() const id = ++nextID return new Promise((resolve, reject) => { parses.set(id, { resolve, reject }) instance.postMessage({ type: "parse", id, text } satisfies MarkdownWorkerRequest) }) } export function projectMarkdown(key: string, text: string, live: boolean) { getWorker() const id = ++nextID return new Promise((resolve, reject) => { projects.set(id, { key, resolve, reject }) projectTransport.send({ type: "project", id, key, text, live }) }) } export function disposeMarkdownProjection(key: string) { projectTransport.dispose(key) projects.forEach((request, id) => { if (request.key !== key) return projects.delete(id) request.reject(new MarkdownWorkerDisposedError()) }) worker?.postMessage({ type: "dispose", key } satisfies MarkdownWorkerRequest) } export function highlightStreamingCode(key: string, text: string, language: string, complete = false) { const instance = getWorker() const id = ++nextID latest.set(key, id) keys.delete(key) keys.add(key) if (keys.size > 200) disposeStreamingCode(keys.values().next().value!) return new Promise((resolve, reject) => { pending.set(id, { key, complete, resolve, reject }) transport.send({ type: "highlight", id, key, text, language, complete }) }) } export function disposeStreamingCode(key: string) { keys.delete(key) latest.delete(key) states.delete(key) transport.dispose(key) pending.forEach((request, id) => { if (request.key !== key) return pending.delete(id) request.reject(new MarkdownWorkerDisposedError()) }) worker?.postMessage({ type: "dispose", key } satisfies MarkdownWorkerRequest) } export class MarkdownWorkerDisposedError extends Error {} export class MarkdownWorkerSupersededError extends Error {} export class MarkdownWorkerUnavailableError extends Error {} function getWorker() { if (worker) return worker if (disabled) throw new MarkdownWorkerUnavailableError(disabled.message) try { worker = new Worker(MarkdownWorkerUrl, { type: "module" }) } catch (error) { disabled = error instanceof Error ? error : new Error(String(error)) throw new MarkdownWorkerUnavailableError(disabled.message) } worker.onmessage = (event: MessageEvent) => { if (event.data.type === "parse") { const result = parses.get(event.data.id) if (!result) return parses.delete(event.data.id) result.resolve(event.data.html) return } if (event.data.type === "project") { const result = projects.get(event.data.id) if (!result) { projectTransport.complete(event.data.key, event.data.id) return } projects.delete(event.data.id) result.resolve(event.data.projection) projectTransport.complete(event.data.key, event.data.id) return } if (event.data.type === "error") { const parsed = parses.get(event.data.id) if (parsed) { parses.delete(event.data.id) parsed.reject(new Error(event.data.message)) return } const projected = projects.get(event.data.id) if (projected) { projects.delete(event.data.id) projected.reject(new Error(event.data.message)) projectTransport.complete(projected.key, event.data.id) return } } if (event.data.type === "superseded") { const projected = projects.get(event.data.id) if (projected) { projects.delete(event.data.id) projected.reject(new MarkdownWorkerSupersededError()) projectTransport.complete(projected.key, event.data.id) return } } const key = event.data.key if (!key) return const result = pending.get(event.data.id) if (!result) { transport.complete(key, event.data.id) return } pending.delete(event.data.id) if (!keys.has(key)) { result.reject(new MarkdownWorkerDisposedError()) transport.complete(key, event.data.id) return } if (event.data.type === "superseded") { result.reject(new MarkdownWorkerSupersededError()) transport.complete(key, event.data.id) return } if (event.data.type === "error") { result.reject(new Error(event.data.message)) transport.complete(key, event.data.id) return } const state = applyMarkdownWorkerResponse(states.get(key), event.data) if (shouldReleaseMarkdownWorkerState(result.complete, latest.get(key), event.data.id)) { states.delete(key) keys.delete(key) latest.delete(key) } else states.set(key, state) result.resolve(state) transport.complete(key, event.data.id) } const fail = (message: string) => { const error = new Error(message) disabled = error transport.reset() projectTransport.reset() pending.forEach((request) => request.reject(error)) projects.forEach((request) => request.reject(error)) parses.forEach((request) => request.reject(error)) pending.clear() projects.clear() parses.clear() states.clear() keys.clear() latest.clear() worker?.terminate() worker = undefined } worker.onerror = (event) => fail(event.message || "Markdown highlighting worker failed") worker.onmessageerror = () => fail("Markdown worker response failed") return worker }