import type { FileDiffMetadata } from "@pierre/diffs"; import { streamPatchInWorker } from "$lib/components/repo/diffRpc"; /** * A patch being parsed in the worker while it downloads. Starting one does not * wait for anything else on the page: the file list and the patch describe the * same commit, and fetching them one after the other costs the sum of both. */ export interface PatchStream { /** parsed files, keyed by current name and by previous name for renames */ readonly byName: Map; /** called with each batch as it lands, and unsubscribed on teardown */ subscribe: (listener: (files: FileDiffMetadata[]) => void) => () => void; /** false when the patch could not be read, so a caller can fall back */ readonly done: Promise; readonly lines: number; readonly subscribed: boolean; stop: () => void; } // Dedupes in-flight patch streams by URL. const live = new Map(); const KEEP = 2; const MAX_RETAINED_LINES = 20_000; // a navigation leaves its patch behind; keeping the last couple means going // back to a commit is instant, and anything older is dropped const trim = () => { for (;;) { const evictable = [...live].filter( ([, stream], index, all) => index < all.length - 1 && !stream.subscribed ); if (!evictable.length) return; let victim: [string, PatchStream] | undefined; if (live.size > KEEP) victim = evictable[0]; else if ( [...live.values()].reduce((sum, stream) => sum + stream.lines, 0) > MAX_RETAINED_LINES ) victim = evictable.reduce((a, b) => (b[1].lines > a[1].lines ? b : a)); if (!victim) return; victim[1].stop(); live.delete(victim[0]); } }; export const getPatchStream = (url: string): PatchStream => { let stream = live.get(url); if (stream) live.delete(url); else stream = startPatchStream(url, trim); live.set(url, stream); trim(); return stream; }; export const startPatchStream = (url: string, onGrow?: () => void): PatchStream => { const byName = new Map(); const listeners = new Set<(files: FileDiffMetadata[]) => void>(); const controller = new AbortController(); let stopped = false; let lines = 0; const land = (files: FileDiffMetadata[]) => { if (stopped) return; for (const file of files) { lines += file.unifiedLineCount; byName.set(file.name, file); if (file.prevName) byName.set(file.prevName, file); } for (const listener of listeners) listener(files); onGrow?.(); }; const stream: PatchStream = { byName, get lines() { return lines; }, get subscribed() { return listeners.size > 0; }, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, // resolved either way: a caller that never asks cannot leave a rejection // unhandled, and one that does gets an answer instead of an exception done: streamPatchInWorker(url, land, controller.signal).then( () => true, () => false ), stop: () => { stopped = true; listeners.clear(); controller.abort(); } }; return stream; };