/** * The state behind the history view. * * Owns the stream and the rows it has produced so far, so components can stay * about rendering. */ import { createSignal } from "solid-js"; import { streamGraph, type GraphStream } from "./api"; import type { GraphRow, RepositoryId } from "./bindings"; import { isGigitError } from "./bindings"; /** * Rows asked for up front. Comfortably more than a screenful, so the first * scroll does not have to wait for a round trip. */ const INITIAL_BUDGET = 300; /** Rows asked for each time the view nears the end of what it has. */ const TOP_UP = 400; export type GraphStatus = "idle" | "loading" | "streaming" | "complete" | "failed"; export function createGraphView() { const [rows, setRows] = createSignal([]); const [status, setStatus] = createSignal("idle"); const [message, setMessage] = createSignal(); let stream: GraphStream | undefined; /** Guards against asking for more while a top-up is already in flight. */ let waiting = false; function receive(event: Parameters[2]>[0]) { switch (event.event) { case "rows": { waiting = false; // `start` is the row index of the batch, so a batch that somehow // arrives out of order still lands in the right place. setRows((current) => { const next = current.slice(0, event.data.start); next.push(...event.data.rows); return next; }); break; } case "exhausted": { waiting = false; setStatus("complete"); if (event.data.degraded) { setMessage( "This history's dates are skewed enough that some commits may be a row or two out of place.", ); } break; } case "failed": { waiting = false; setStatus("failed"); setMessage(event.data.message); break; } } } /** Start drawing a repository's history from the newest commit. */ async function show(id: RepositoryId) { setRows([]); setMessage(undefined); setStatus("loading"); waiting = false; try { stream = await streamGraph(id, INITIAL_BUDGET, receive); setStatus((current) => (current === "loading" ? "streaming" : current)); } catch (error) { setStatus("failed"); setMessage(isGigitError(error) ? error.message : String(error)); } } /** * Ask for more rows. Safe to call on every scroll — it does nothing while a * top-up is already on its way, or once the history is fully drawn. */ function loadMore() { if (waiting || !stream || status() !== "streaming") return; waiting = true; stream.requestMore(TOP_UP).catch((error: unknown) => { waiting = false; setStatus("failed"); setMessage(isGigitError(error) ? error.message : String(error)); }); } return { rows, status, message, show, loadMore }; } export type GraphView = ReturnType;