import { parseDiffFromFile, processPatch, type FileContents, type FileDiffMetadata } from "@pierre/diffs"; export interface DiffFileRequest { id: number; kind: "file"; oldFile: FileContents; newFile: FileContents; } export interface DiffStreamRequest { id: number; kind: "stream"; /** transferred from the main thread, which is where the preload lives */ body?: ReadableStream; /** fallback for browsers that will not transfer a stream */ url?: string; } export interface DiffCancelRequest { id: number; kind: "cancel"; } export type DiffWorkerRequest = DiffFileRequest | DiffStreamRequest | DiffCancelRequest; export interface DiffWorkerResponse { id: number; fileDiff?: FileDiffMetadata; files?: FileDiffMetadata[]; /** a stream sends many responses; the last one carries done */ done?: boolean; error?: string; } // Pierre's worker requires cacheKey to avoid warnings and cache syntax highlighting. const withCacheKeys = (files: FileDiffMetadata[]): FileDiffMetadata[] => { for (const file of files) file.cacheKey ??= `${file.prevObjectId ?? ""}:${file.newObjectId ?? ""}:${file.name}`; return files; }; const handle = (request: DiffFileRequest): DiffWorkerResponse => ({ id: request.id, fileDiff: parseDiffFromFile(request.oldFile, request.newFile) }); // Every file section in a git format-patch starts with FILE_MARK. const FILE_MARK = "\ndiff --git "; // enough files to be worth a message, few enough that the first ones paint while // the rest of a multi-megabyte body is still arriving const BATCH = 25; // a page that navigated away is not owed the rest of its patch const cancelled = new Set(); const streamPatch = async (request: DiffStreamRequest): Promise => { // a transferred body is already downloading: the document asked for it before // this worker existed. only fetch here when nothing was handed over const body = request.body ?? (await (async () => { if (!request.url) throw new Error("patch stream has neither body nor url"); const response = await fetch(request.url); if (!response.ok || !response.body) throw new Error(`patch ${response.status}`); return response.body; })()); const reader = body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let batch: FileDiffMetadata[] = []; const flush = (done: boolean) => { if (!done && batch.length === 0) return; self.postMessage({ id: request.id, files: batch, done } satisfies DiffWorkerResponse); batch = []; }; for (;;) { if (cancelled.has(request.id)) { cancelled.delete(request.id); await reader.cancel().catch(() => undefined); return; } const { value, done } = await reader.read(); if (value) buffer += decoder.decode(value, { stream: true }); // skip the mark at index 0: a section always opens with one for ( let mark = buffer.indexOf(FILE_MARK, 1); mark !== -1; mark = buffer.indexOf(FILE_MARK, 1) ) { const section = buffer.slice(0, mark + 1); buffer = buffer.slice(mark + 1); batch.push(...withCacheKeys(processPatch(section).files)); if (batch.length >= BATCH) flush(false); } if (done) break; } buffer += decoder.decode(); if (buffer.trim()) batch.push(...withCacheKeys(processPatch(buffer).files)); flush(true); }; self.onmessage = (e: MessageEvent) => { const fail = (error: unknown) => self.postMessage({ id: e.data.id, done: true, error: error instanceof Error ? error.message : String(error) } satisfies DiffWorkerResponse); try { if (e.data.kind === "cancel") { cancelled.add(e.data.id); return; } if (e.data.kind === "stream") { void streamPatch(e.data).catch(fail); return; } self.postMessage(handle(e.data) satisfies DiffWorkerResponse); } catch (error) { fail(error); } };