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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
128 lines · 4.6 KB Raw History
// What a page follows as it happens, from knotless-serve's `/_live` server-
// sent events: a repository written about draws its page again, and a
// running check's output shows as its runner sends it. A browser that loses
// the connection asks again by itself, from where it was. A page left out of
// sight a while, or offline, asks its answers again as it is back.

import { invalidateAll } from '$app/navigation';

/** How long after a write the page waits for the rest of a burst before it asks its answers again. */
const SETTLE = 300;

/** How long a tab stays out of sight before its page, looked at again, asks its answers again. */
const AWAY = 60_000;

let settling: ReturnType<typeof setTimeout> | undefined;
/** Whether the page may have missed a change while out of sight. */
let missed = false;

/** Asks the page's answers again, once for a burst of reasons. */
function again() {
	clearTimeout(settling);
	settling = setTimeout(() => {
		missed = false;
		void invalidateAll();
	}, SETTLE);
}

/** The page may have missed a change: it asks again now if in sight, or once it is looked at. */
function lost() {
	if (document.visibilityState === 'visible') again();
	else missed = true;
}

/** Asks the page's answers again as the tab is looked at after a minute away, or after missing a change, and as the browser is back online; returns what stops it. */
export function followReturns(): () => void {
	let hidden: number | undefined;
	const seen = () => {
		if (document.visibilityState === 'hidden') {
			hidden = Date.now();
			return;
		}
		if (missed || (hidden !== undefined && Date.now() - hidden >= AWAY)) again();
		hidden = undefined;
	};
	document.addEventListener('visibilitychange', seen);
	window.addEventListener('online', lost);
	return () => {
		document.removeEventListener('visibilitychange', seen);
		window.removeEventListener('online', lost);
	};
}

/** Asks the page's answers again each time the repository `uri` is written about, and once the connection is back after it dropped, as a write meanwhile was not heard; returns what stops it. */
export function followRepo(uri: string): () => void {
	const events = new EventSource(`/_live/repo?repo=${encodeURIComponent(uri)}`);
	let dropped = false;
	events.addEventListener('changed', again);
	events.addEventListener('error', () => (dropped = true));
	events.addEventListener('open', () => {
		if (dropped) lost();
		dropped = false;
	});
	return () => events.close();
}

/** A step of a running check that ended, as its runner says. */
export interface LiveStep {
	name: string;
	status: string;
	exitCode?: number;
	durationMs?: number;
}

/** What a running check has printed so far, its steps that ended, the one going, and whether all of them did. */
export interface LiveRun {
	text: string;
	steps: LiveStep[];
	going?: string;
	ended: boolean;
}

/** Follows the running check `uri` of the repository `repo`, calling `heard` with all of it so far as more comes; returns what stops it. */
export function followRun(repo: string, uri: string, heard: (run: LiveRun) => void): () => void {
	const query = `repo=${encodeURIComponent(repo)}&uri=${encodeURIComponent(uri)}`;
	const events = new EventSource(`/_live/check?${query}`);
	const run: LiveRun = { text: '', steps: [], ended: false };
	events.addEventListener('output', (e) => {
		run.text += JSON.parse(e.data) as string;
		heard({ ...run });
	});
	events.addEventListener('steps', (e) => {
		run.steps = JSON.parse(e.data) as LiveStep[];
		heard({ ...run });
	});
	events.addEventListener('going', (e) => {
		run.going = (JSON.parse(e.data) as string | null) ?? undefined;
		heard({ ...run });
	});
	events.addEventListener('ended', () => {
		run.ended = true;
		events.close();
		heard({ ...run });
	});
	return () => events.close();
}

/** How near its end, in pixels, a log scrolled counts as at it. */
const NEAR_END = 24;

/** Keeps the scrolling `node` at its end as what it shows grows, while it is at its end: scrolled up from it, to read, it stays where it is until scrolled back down. It watches the text itself, as an action's update runs before the text it is given is drawn. */
export function followsEnd(node: HTMLElement) {
	let atEnd = true;
	const scrolled = () =>
		(atEnd = node.scrollHeight - node.scrollTop - node.clientHeight < NEAR_END);
	const toEnd = () => {
		if (atEnd) node.scrollTop = node.scrollHeight;
	};
	const grown = new MutationObserver(toEnd);
	grown.observe(node, { childList: true, characterData: true, subtree: true });
	node.addEventListener('scroll', scrolled);
	toEnd();
	return {
		destroy() {
			grown.disconnect();
			node.removeEventListener('scroll', scrolled);
		}
	};
}