~james/core

ref: 452f17b75181d62d1d92016edecd38e1d70fecab core/web/src/lib/api/patchStream.ts -rw-r--r-- 3.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import type { FileDiffMetadata } from "@pierre/diffs";
import { streamPatchInWorker } from "#lib/components/repo/diffRpc.ts";

/**
 * 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.
 *
 * Only ever for a single commit: format-patch serves one patch per commit and
 * files are keyed by name, so over a longer range a file several commits touch
 * keeps one commit's hunks. Longer ranges take the combined diff instead.
 */
export interface PatchStream {
	/** parsed files, keyed by current name and by previous name for renames */
	readonly byName: Map<string, FileDiffMetadata>;
	/** 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<boolean>;
	readonly lines: number;
	readonly subscribed: boolean;
	stop: () => void;
}

// Dedupes in-flight patch streams by URL.
const live = new Map<string, PatchStream>();
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<string, FileDiffMetadata>();
	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;
};