Skip to content
106 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-a432c4ed9deb834b1'1import type { FileDiff } from "@g1t/contracts";
2
3/**
4 * Fetching the files a large change was sent without (lib/diff.ts
5 * firstScreens), from routes/repo/diff-files.ts. Files asked for in the
6 * same moment, as several scroll near together, go in one request, and
7 * each is fetched once per page: a refresh or a file folded and unfolded
8 * finds it here.
9 */
10
11/** As many as the route takes in one request. */
12const PER_REQUEST = 20;
13/** How long asks are gathered before they are sent. */
14const GATHER_MS = 30;
15
16type Ask = { path: string; resolve: (file: FileDiff | null) => void; reject: (error: unknown) => void };
17
18const loaded = new Map<string, FileDiff | null>();
19const loading = new Map<string, Promise<FileDiff | null>>();
20const queued = new Map<string, Ask[]>();
21let timer: ReturnType<typeof setTimeout> | null = null;
22
23const keyOf = (source: string, path: string) => `${source}\n${path}`;
24
25/** A file fetched already, so it is drawn at once rather than after a frame of its placeholder. */
26export function peekFile(source: string, path: string): FileDiff | null | undefined {
27 return loaded.get(keyOf(source, path));
28}
29
30/**
31 * The file at `path` of the change `source` names (the route's address with
32 * its `base` and `head`), with its lines; null when the change no longer has it.
33 * The files in `also` (the ones after it) come in the same request, so a
34 * reader scrolling down finds them fetched rather than asking one by one.
35 */
36export function loadFile(
37 source: string,
38 path: string,
39 { also = [], fetcher = fetch }: { also?: string[]; fetcher?: typeof fetch } = {},
40): Promise<FileDiff | null> {
41 const key = keyOf(source, path);
42 // Fetched already, or on its way (with the file before it): nothing to send.
43 if (loaded.has(key)) return Promise.resolve(loaded.get(key)!);
44 if (loading.has(key)) return loading.get(key)!;
45 for (const next of also) {
46 if (!loaded.has(keyOf(source, next))) void ask(source, next, fetcher).catch(() => {});
47 }
48 return ask(source, path, fetcher);
49}
50
51function ask(source: string, path: string, fetcher: typeof fetch): Promise<FileDiff | null> {
52 const key = keyOf(source, path);
53 if (loaded.has(key)) return Promise.resolve(loaded.get(key)!);
54 const pending = loading.get(key);
55 if (pending) return pending;
56 const promise = new Promise<FileDiff | null>((resolve, reject) => {
57 const asks = queued.get(source) ?? [];
58 asks.push({ path, resolve, reject });
59 queued.set(source, asks);
60 timer ??= setTimeout(() => flush(fetcher), GATHER_MS);
61 }).then(
62 (file) => {
63 loaded.set(key, file);
64 loading.delete(key);
65 return file;
66 },
67 (error: unknown) => {
68 // Asked again, it is fetched again.
69 loading.delete(key);
70 throw error;
71 },
72 );
73 loading.set(key, promise);
74 return promise;
75}
76
77function flush(fetcher: typeof fetch) {
78 timer = null;
79 const all = [...queued];
80 queued.clear();
81 for (const [source, asks] of all) {
82 for (let start = 0; start < asks.length; start += PER_REQUEST) {
83 const batch = asks.slice(start, start + PER_REQUEST);
84 const url = `${source}${source.includes("?") ? "&" : "?"}${batch.map((ask) => `path=${encodeURIComponent(ask.path)}`).join("&")}`;
85 fetcher(url, { headers: { accept: "application/json" } })
86 .then(async (response) => {
87 if (!response.ok) throw new Error(`The diff could not be loaded (${response.status}).`);
88 const { files } = (await response.json()) as { files: FileDiff[] };
89 const byPath = new Map(files.map((file) => [file.path, file]));
90 for (const ask of batch) ask.resolve(byPath.get(ask.path) ?? null);
91 })
92 .catch((error: unknown) => {
93 for (const ask of batch) ask.reject(error);
94 });
95 }
96 }
97}
98
99/** For tests: forget everything fetched. */
100export function forgetFiles() {
101 loaded.clear();
102 loading.clear();
103 queued.clear();
104 if (timer) clearTimeout(timer);
105 timer = null;
106}

This file's history is long; its oldest lines are credited to the oldest commit read.