markdown-worker.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import MarkdownShikiWorkerUrl from "./markdown-shiki.worker.ts?worker&url"
  2. import { KirinCodeTheme } from "@kirincode-ai/ui/context/marked"
  3. import {
  4. applyMarkdownWorkerResponse,
  5. shouldReleaseMarkdownWorkerState,
  6. type MarkdownWorkerRequest,
  7. type MarkdownWorkerResponse,
  8. type MarkdownWorkerState,
  9. } from "./markdown-worker-protocol"
  10. import { createWorkerTransport } from "./markdown-worker-transport"
  11. type Pending = {
  12. key: string
  13. complete: boolean
  14. resolve: (state: MarkdownWorkerState) => void
  15. reject: (error: Error) => void
  16. }
  17. let worker: Worker | undefined
  18. let disabled: Error | undefined
  19. let nextID = 0
  20. const pending = new Map<number, Pending>()
  21. const states = new Map<string, MarkdownWorkerState>()
  22. const keys = new Set<string>()
  23. const latest = new Map<string, number>()
  24. const transport = createWorkerTransport<Extract<MarkdownWorkerRequest, { type: "highlight" }>>({
  25. post: (request) => worker!.postMessage(request),
  26. supersede: (request) => {
  27. const result = pending.get(request.id)
  28. if (!result) return
  29. pending.delete(request.id)
  30. result.reject(new MarkdownWorkerSupersededError())
  31. },
  32. })
  33. export function highlightStreamingCode(key: string, text: string, language: string, complete = false) {
  34. const instance = getWorker()
  35. const id = ++nextID
  36. latest.set(key, id)
  37. keys.delete(key)
  38. keys.add(key)
  39. if (keys.size > 200) disposeStreamingCode(keys.values().next().value!)
  40. return new Promise<MarkdownWorkerState>((resolve, reject) => {
  41. pending.set(id, { key, complete, resolve, reject })
  42. transport.send({ type: "highlight", id, key, text, language, complete })
  43. })
  44. }
  45. export function disposeStreamingCode(key: string) {
  46. keys.delete(key)
  47. latest.delete(key)
  48. states.delete(key)
  49. transport.dispose(key)
  50. pending.forEach((request, id) => {
  51. if (request.key !== key) return
  52. pending.delete(id)
  53. request.reject(new MarkdownWorkerDisposedError())
  54. })
  55. worker?.postMessage({ type: "dispose", key } satisfies MarkdownWorkerRequest)
  56. }
  57. export class MarkdownWorkerDisposedError extends Error {}
  58. export class MarkdownWorkerSupersededError extends Error {}
  59. export class MarkdownWorkerUnavailableError extends Error {}
  60. function getWorker() {
  61. if (worker) return worker
  62. if (disabled) throw new MarkdownWorkerUnavailableError(disabled.message)
  63. try {
  64. worker = new Worker(MarkdownShikiWorkerUrl, { type: "module" })
  65. } catch (error) {
  66. disabled = error instanceof Error ? error : new Error(String(error))
  67. throw new MarkdownWorkerUnavailableError(disabled.message)
  68. }
  69. worker.onmessage = (event: MessageEvent<MarkdownWorkerResponse>) => {
  70. const result = pending.get(event.data.id)
  71. if (!result) {
  72. transport.complete(event.data.key, event.data.id)
  73. return
  74. }
  75. pending.delete(event.data.id)
  76. if (!keys.has(event.data.key)) {
  77. result.reject(new MarkdownWorkerDisposedError())
  78. transport.complete(event.data.key, event.data.id)
  79. return
  80. }
  81. if (event.data.type === "superseded") {
  82. result.reject(new MarkdownWorkerSupersededError())
  83. transport.complete(event.data.key, event.data.id)
  84. return
  85. }
  86. if (event.data.type === "error") {
  87. result.reject(new Error(event.data.message))
  88. transport.complete(event.data.key, event.data.id)
  89. return
  90. }
  91. const state = applyMarkdownWorkerResponse(states.get(event.data.key), event.data)
  92. if (shouldReleaseMarkdownWorkerState(result.complete, latest.get(event.data.key), event.data.id)) {
  93. states.delete(event.data.key)
  94. keys.delete(event.data.key)
  95. latest.delete(event.data.key)
  96. } else states.set(event.data.key, state)
  97. result.resolve(state)
  98. transport.complete(event.data.key, event.data.id)
  99. }
  100. const fail = (message: string) => {
  101. const error = new Error(message)
  102. disabled = error
  103. transport.reset()
  104. pending.forEach((request) => request.reject(error))
  105. pending.clear()
  106. states.clear()
  107. keys.clear()
  108. latest.clear()
  109. worker?.terminate()
  110. worker = undefined
  111. }
  112. worker.onerror = (event) => fail(event.message || "Markdown highlighting worker failed")
  113. worker.onmessageerror = () => fail("Markdown worker response failed")
  114. worker.postMessage({ type: "init", theme: KirinCodeTheme } satisfies MarkdownWorkerRequest)
  115. return worker
  116. }