~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
95 lines · 3.3 KB Raw History
// 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);
	};
}