johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
// 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<FileDiffView[]>;
export class LaterRows {
/** Rows fetched here, why a fetch failed, and the large files asked for, by index. */
fetched = $state<Record<number, FileDiffView>>({});
failed = $state<Record<number, string>>({});
opened = $state<Record<number, boolean>>({});
#asked = new Set<number>();
#queue: number[] = [];
#flushing = false;
#files: () => FileDiffView[];
#first: () => Map<number, FileDiffView>;
#more: More;
/** Of `files`, those `first` holds came with their rows; `more` fetches the rest. */
constructor(files: () => FileDiffView[], first: () => Map<number, FileDiffView>, 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);
};
}