| 1 | /** |
| 2 | * Home that updates live: which of its repositories' changes it shows, |
| 3 | * and which rows are new since it last drew. Pure; the hook that uses |
| 4 | * them is lib/use-home-live.ts. |
| 5 | * |
| 6 | * Home watches the workspace's projects (the first HOME_WATCH_MAX, as the |
| 7 | * digest reads them) over the live socket, as a repository page |
| 8 | * watches its own (lib/repo-live.ts), and refreshes when something it |
| 9 | * lists moves: a push to a default branch, a pull request or review, an |
| 10 | * issue or comment, a run starting or finishing, a deployment or release, |
| 11 | * a session starting or ending. Not a branch's pushes or a commit's |
| 12 | * checks: a busy workspace sends those all the time, and nothing on Home |
| 13 | * shows them. Its loader reads many services, so it refreshes at most |
| 14 | * every HOME_GAP_MS. |
| 15 | */ |
| 16 | import type { RepoChange } from "@g1t/contracts"; |
| 17 | |
| 18 | import { branchOf } from "./repo-live.ts"; |
| 19 | |
| 20 | /** The most projects Home watches (a person watches at most 50, other tabs' pages too). */ |
| 21 | export const HOME_WATCH_MAX = 25; |
| 22 | /** However many changes arrive, Home refreshes at most this often. */ |
| 23 | export const HOME_GAP_MS = 5000; |
| 24 | |
| 25 | /** A project Home watches: its id, `owner/name` and default branch. */ |
| 26 | export type HomeRepo = { id: string; path: string; defaultBranch: string | null }; |
| 27 | |
| 28 | const FAMILIES = ["pull", "review", "issue", "comment", "workflow", "run", "deployment", "deployment_status", "release", "package"]; |
| 29 | const KINDS = ["session.started", "session.ended", "queue.changed"]; |
| 30 | |
| 31 | /** Whether a change moves something Home shows. */ |
| 32 | export function homeShows(change: RepoChange, repos: readonly HomeRepo[]): boolean { |
| 33 | if (change.kind === "git.push") { |
| 34 | const repo = repos.find((one) => one.id === change.repo_id); |
| 35 | const branch = branchOf(change.ref); |
| 36 | // A default branch's push lands; a tag's is a release. |
| 37 | return branch == null || !repo?.defaultBranch || branch === repo.defaultBranch; |
| 38 | } |
| 39 | return FAMILIES.includes(change.kind.split(".")[0] ?? "") || KINDS.includes(change.kind); |
| 40 | } |
| 41 | |
| 42 | /** What a list on the page remembers between draws: the keys it has shown, under which scope. */ |
| 43 | export type Seen = { scope: string; keys: ReadonlySet<string> }; |
| 44 | |
| 45 | /** |
| 46 | * The rows of `keys` that were not on the list when it last drew, and |
| 47 | * what to remember now. On a first draw, or once the scope changed (the |
| 48 | * span switched), none are new: only what arrives while it is open is. |
| 49 | */ |
| 50 | export function arrivedOf(seen: Seen | null, scope: string, keys: readonly string[]): { arrived: ReadonlySet<string>; seen: Seen } { |
| 51 | if (!seen || seen.scope !== scope) return { arrived: new Set(), seen: { scope, keys: new Set(keys) } }; |
| 52 | const arrived = new Set(keys.filter((key) => !seen.keys.has(key))); |
| 53 | return { arrived, seen: { scope, keys: new Set([...seen.keys, ...keys]) } }; |
| 54 | } |