highlightWorker.ts
| 1 | import config from "../config.ts"; |
| 2 | import { MAX_DIFF_CACHE } from "../constants.ts"; |
| 3 | import type { ParsedFile, RenderedDiffFile } from "./diffHighlight.ts"; |
| 4 | import { parseDiff } from "./diffHighlight.ts"; |
| 5 | import type { FileView } from "./highlight.ts"; |
| 6 | import { hasBinaryContent } from "./highlight.ts"; |
| 7 | |
| 8 | export { hasBinaryContent }; |
| 9 | |
| 10 | // ─── Worker pool ────────────────────────────────────────────────────────────── |
| 11 | |
| 12 | interface WorkerHandle { |
| 13 | worker: Worker; |
| 14 | ready: Promise<void>; |
| 15 | pending: number; |
| 16 | handlers: Map< |
| 17 | number, |
| 18 | { resolve: (v: unknown) => void; reject: (e: unknown) => void } |
| 19 | >; |
| 20 | } |
| 21 | |
| 22 | let nextId = 0; |
| 23 | |
| 24 | function createHandle(): WorkerHandle { |
| 25 | const handlers = new Map< |
| 26 | number, |
| 27 | { resolve: (v: unknown) => void; reject: (e: unknown) => void } |
| 28 | >(); |
| 29 | const handle: WorkerHandle = { |
| 30 | worker: null!, |
| 31 | ready: null!, |
| 32 | pending: 0, |
| 33 | handlers, |
| 34 | }; |
| 35 | handle.worker = new Worker( |
| 36 | new URL("../workers/highlight.worker.ts", import.meta.url).href, |
| 37 | ); |
| 38 | handle.ready = new Promise<void>((resolve) => { |
| 39 | handle.worker.onmessage = (event: MessageEvent) => { |
| 40 | if (event.data.type === "ready") { |
| 41 | resolve(); |
| 42 | handle.worker.onmessage = (e) => onMessage(handle, e); |
| 43 | return; |
| 44 | } |
| 45 | onMessage(handle, event); |
| 46 | }; |
| 47 | }); |
| 48 | return handle; |
| 49 | } |
| 50 | |
| 51 | function onMessage(handle: WorkerHandle, event: MessageEvent) { |
| 52 | const { id, result, error } = event.data; |
| 53 | const p = handle.handlers.get(id); |
| 54 | if (!p) return; |
| 55 | handle.handlers.delete(id); |
| 56 | handle.pending--; |
| 57 | if (error !== undefined) p.reject(new Error(error)); |
| 58 | else p.resolve(result); |
| 59 | } |
| 60 | |
| 61 | const pool: WorkerHandle[] = Array.from( |
| 62 | { length: config.HIGHLIGHT_WORKERS }, |
| 63 | createHandle, |
| 64 | ); |
| 65 | |
| 66 | function leastBusy(): WorkerHandle { |
| 67 | return pool.reduce((a, b) => (a.pending <= b.pending ? a : b)); |
| 68 | } |
| 69 | |
| 70 | function request<T>(msg: Record<string, unknown>): Promise<T> { |
| 71 | const handle = leastBusy(); |
| 72 | const id = nextId++; |
| 73 | handle.pending++; |
| 74 | return new Promise<T>((resolve, reject) => { |
| 75 | handle.handlers.set(id, { |
| 76 | resolve: resolve as (v: unknown) => void, |
| 77 | reject, |
| 78 | }); |
| 79 | handle.ready.then(() => handle.worker.postMessage({ id, ...msg })); |
| 80 | }); |
| 81 | } |
| 82 | |
| 83 | // ─── Diff cache (lives on main thread now that dispatch is per-file) ────────── |
| 84 | |
| 85 | const diffCache = new Map<string, RenderedDiffFile[]>(); |
| 86 | |
| 87 | // ─── Public API ─────────────────────────────────────────────────────────────── |
| 88 | |
| 89 | export function serveFile( |
| 90 | content: Buffer, |
| 91 | filename: string, |
| 92 | cacheKey: string, |
| 93 | ): Promise<FileView> { |
| 94 | return request({ |
| 95 | type: "serveFile", |
| 96 | content: content.buffer, |
| 97 | filename, |
| 98 | cacheKey, |
| 99 | }); |
| 100 | } |
| 101 | |
| 102 | export async function prepareDiff( |
| 103 | rawDiff: string, |
| 104 | cacheKey: string, |
| 105 | repoName?: string, |
| 106 | ): Promise<RenderedDiffFile[]> { |
| 107 | const cached = diffCache.get(cacheKey); |
| 108 | if (cached) return cached; |
| 109 | |
| 110 | const parsed = await parseDiff(rawDiff, repoName); |
| 111 | const result = await Promise.all( |
| 112 | parsed.map((file: ParsedFile) => |
| 113 | request<RenderedDiffFile>({ type: "highlightFile", file }), |
| 114 | ), |
| 115 | ); |
| 116 | |
| 117 | if (cacheKey) { |
| 118 | if (diffCache.size >= MAX_DIFF_CACHE) |
| 119 | diffCache.delete(diffCache.keys().next().value!); |
| 120 | diffCache.set(cacheKey, result); |
| 121 | } |
| 122 | return result; |
| 123 | } |
| 124 |