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";
6
7// ─── Worker pool ──────────────────────────────────────────────────────────────
8
9interface 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
19let nextId = 0;
20
21function 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
48function 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
58let pool: WorkerHandle[] | null = null;
59
60function 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
65function 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
80const diffCache = new Map<string, RenderedDiffFile[]>();
81
82// ─── Public API ───────────────────────────────────────────────────────────────
83
84export 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
97export 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