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