// The rows of a diff's files fetched as a reader comes to them: a pull's, // listed by dev.knotless.getThread and filled by getPullDiff, or a // commit's, by getCommit and its `paths`. Those that came with their rows // show at once; the rest are asked for a batch at a time as each nears // the screen, but for one so large it waits to be asked for. import type { FileDiffView } from '#lib/lexicons/dev/knotless/defs.ts'; import { BATCH, held, matched, pathsOf, unfetched } from '#lib/diffs.ts'; /** The files `paths` names, rows and all, or why they could not be read. */ export type More = (paths: string[]) => Promise; export class LaterRows { /** Rows fetched here, why a fetch failed, and the large files asked for, by index. */ fetched = $state>({}); failed = $state>({}); opened = $state>({}); #asked = new Set(); #queue: number[] = []; #flushing = false; #files: () => FileDiffView[]; #first: () => Map; #more: More; /** Of `files`, those `first` holds came with their rows; `more` fetches the rest. */ constructor(files: () => FileDiffView[], first: () => Map, more: More) { this.#files = files; this.#first = first; this.#more = more; } /** The file at `i` with its rows, once it has them. */ rowsOf = (i: number): FileDiffView | undefined => this.fetched[i] ?? this.#first().get(i); /** How the file at `i` waits for its rows, if it does: being fetched, held back, or failed. */ waiting = (i: number) => { const f = this.#files()[i]; if (!unfetched(f) || this.rowsOf(i)) return undefined; if (i in this.failed) return { failed: this.failed[i] }; if (held(f) && !this.opened[i]) return { held: true as const }; return { loading: true as const }; }; #want(i: number) { if (!unfetched(this.#files()[i]) || this.rowsOf(i) || this.#asked.has(i)) return; this.#asked.add(i); this.#queue.push(i); if (!this.#flushing) void this.#flush(); } /** Asks for what is wanted, a batch at a time, as the reader scrolls on. */ async #flush() { this.#flushing = true; await Promise.resolve(); while (this.#queue.length) { const files = this.#files(); const batch = this.#queue.splice(0, BATCH); const paths = batch.flatMap((i) => pathsOf(files[i])); const got = await this.#more(paths).then( (a) => matched(files, batch, a), (e: Error) => e.message ); for (const i of batch) { const f = typeof got === 'string' ? undefined : got.get(i); if (f) this.fetched[i] = f; else { this.failed[i] = typeof got === 'string' ? got : 'Its diff could not be read.'; this.#asked.delete(i); } } } this.#flushing = false; } /** An action: fetches the rows of the file at `i` once it nears the screen, unless it waits to be asked for. */ seen = (node: HTMLElement, i: number) => { const watch = new IntersectionObserver( (at) => { if (!at.some((a) => a.isIntersecting)) return; watch.disconnect(); if (!held(this.#files()[i])) this.#want(i); }, { rootMargin: '800px 0px' } ); watch.observe(node); return { destroy: () => watch.disconnect() }; }; /** Fetches the rows of the file at `i` now, as its reader asked. */ askFor = (i: number) => { this.opened[i] = true; delete this.failed[i]; this.#want(i); }; }