opencode / packages /session-ui /src /components /markdown-worker.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
3e05655 verified
Raw
History Blame Contribute Delete
7.21 kB
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<number, HighlightPending>()
const projects = new Map<number, ProjectPending>()
const parses = new Map<number, ParsePending>()
const states = new Map<string, MarkdownWorkerState>()
const keys = new Set<string>()
const latest = new Map<string, number>()
const transport = createWorkerTransport<Extract<MarkdownWorkerRequest, { type: "highlight" }>>({
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<Extract<MarkdownWorkerRequest, { type: "project" }>>({
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<string>((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<Projection>((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<MarkdownWorkerState>((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<MarkdownWorkerResponse>) => {
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
}