highlightWorker.ts
Raw
1import config from "../config.ts";
2import { MAX_DIFF_CACHE } from "../constants.ts";
3import type { ParsedFile, RenderedDiffFile } from "./diffHighlight.ts";
4import { parseDiff } from "./diffHighlight.ts";
5import type { FileView } from "./highlight.ts";
6import { hasBinaryContent } from "./highlight.ts";
7
8export { hasBinaryContent };
9
10// ─── Worker pool ──────────────────────────────────────────────────────────────
11
12interface 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
22let nextId = 0;
23
24function 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
51function 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
61const pool: WorkerHandle[] = Array.from(
62 { length: config.HIGHLIGHT_WORKERS },
63 createHandle,
64);
65
66function leastBusy(): WorkerHandle {
67 return pool.reduce((a, b) => (a.pending <= b.pending ? a : b));
68}
69
70function 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
85const diffCache = new Map<string, RenderedDiffFile[]>();
86
87// ─── Public API ───────────────────────────────────────────────────────────────
88
89export 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
102export 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