Live pages refresh only for what they show, and less often: on a busy repository every branch's pushes and every commit's checks reached the Code page, so it reloaded every few seconds for changes it does not draw. A page of files now hears pushes to its own branch, a branch made or deleted (the branch menu and count), tags, releases, deployments and the checks of the commit it shows, not another branch's pushes or another commit's checks; Commits hears checks on the commits it lists, and the overview its own commit's checks and pull requests' finished ones. Whatever arrives, a page refreshes at most every two seconds, and a tab out of view no longer refreshes once a minute: it waits and catches up once when it comes back. Pages that ask again on a timer while something runs (a pull request's checks, a run, the Work page) count from the page's last refresh, whoever made it, so a live change and the timer no longer refresh twice in a row, and a tab out of view asks once on coming back. Checked: the site's typecheck, its 904 tests (new: what a page of files, Commits and the overview hear, a burst of changes every 100 ms for a minute refreshing no more than once per two seconds yet at least every five, a hidden tab holding with no timer and catching up at once) and a production build.
8 files+169−500/8 viewed
| 5 | 5 | export const REFRESH_MS = 4000; | |
| 6 | 6 | ||
| 7 | 7 | /** | |
| 8 | − | * Revalidates the page every `everyMs` while `on` and the tab is visible. | |
| 8 | + | * Revalidates the page `everyMs` after it last loaded while `on` and the | |
| 9 | + | * tab is visible. Whatever refreshed it last (this, a live change, a form, | |
| 10 | + | * a navigation) starts the count again, so a page that also follows live | |
| 11 | + | * changes is not refreshed twice in a row. Out of view it waits, and asks | |
| 12 | + | * once when the tab comes back if the time has passed meanwhile. | |
| 13 | + | * | |
| 9 | 14 | * Never while a navigation is pending: a revalidation then restarts the | |
| 10 | 15 | * navigation, aborting its loaders, so a click to a page slower than the | |
| 11 | 16 | * interval would never arrive. Never on top of a revalidation still going. | |
| 12 | 17 | */ | |
| 13 | 18 | export function useRefreshWhile(on: boolean, everyMs = REFRESH_MS) { | |
| 14 | 19 | const revalidator = useRevalidator(); | |
| 15 | − | const navigating = useNavigation().state !== "idle"; | |
| 20 | + | const idle = useNavigation().state === "idle" && revalidator.state === "idle"; | |
| 16 | 21 | useEffect(() => { | |
| 17 | − | if (!on || navigating) return; | |
| 18 | − | const timer = setInterval(() => { | |
| 19 | − | if (document.visibilityState === "visible" && revalidator.state === "idle") revalidator.revalidate(); | |
| 20 | − | }, everyMs); | |
| 21 | − | return () => clearInterval(timer); | |
| 22 | − | }, [on, navigating, everyMs, revalidator]); | |
| 22 | + | if (!on || !idle) return; | |
| 23 | + | let due = false; | |
| 24 | + | const ask = () => { | |
| 25 | + | if (document.visibilityState === "visible") revalidator.revalidate(); | |
| 26 | + | else due = true; | |
| 27 | + | }; | |
| 28 | + | const timer = setTimeout(ask, everyMs); | |
| 29 | + | const back = () => { | |
| 30 | + | if (due && document.visibilityState === "visible") ask(); | |
| 31 | + | }; | |
| 32 | + | document.addEventListener("visibilitychange", back); | |
| 33 | + | return () => { | |
| 34 | + | clearTimeout(timer); | |
| 35 | + | document.removeEventListener("visibilitychange", back); | |
| 36 | + | }; | |
| 37 | + | // `revalidator` is a new object whenever its state changes, which `idle` already follows. | |
| 38 | + | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 39 | + | }, [on, idle, everyMs]); | |
| 23 | 40 | } |
| 4 | 4 | import { | |
| 5 | 5 | FALLBACK_FOCUS_MS, | |
| 6 | 6 | FALLBACK_POLL_MS, | |
| 7 | − | HIDDEN_MIN_MS, | |
| 8 | 7 | LIVE_DEBOUNCE_MS, | |
| 9 | 8 | LIVE_HOLD_CHECK_MS, | |
| 10 | 9 | LIVE_MAX_WAIT_MS, | |
| 10 | + | LIVE_MIN_GAP_MS, | |
| 11 | 11 | type RepoChange, | |
| 12 | 12 | atBottom, | |
| 13 | 13 | branchOf, | |
| ⋯ | |||
| 53 | 53 | assert.equal(overview(change("comment.created", { number: 3 })), false); | |
| 54 | 54 | }); | |
| 55 | 55 | ||
| 56 | + | test("the overview hears its own commit's checks, and every pull request's finished ones", () => { | |
| 57 | + | const overview = repoPages.overview("abc"); | |
| 58 | + | assert.equal(overview(change("status.created", { sha: "abc" })), true); | |
| 59 | + | assert.equal(overview(change("run.updated", { sha: "abc", run: "run_1" })), true); | |
| 60 | + | assert.equal(overview(change("status.created", { sha: "f00d" })), false, "another commit's"); | |
| 61 | + | assert.equal(overview(change("run.updated", { sha: "f00d", run: "run_2" })), false); | |
| 62 | + | assert.equal(overview(change("checks.completed", { sha: "f00d", number: 4 })), true, "how often checks pass"); | |
| 63 | + | assert.equal(overview(change("status.created")), true, "a check that names no commit may be its"); | |
| 64 | + | }); | |
| 65 | + | ||
| 56 | 66 | test("the files page follows refs, checks, releases and deployments", () => { | |
| 57 | 67 | const files = repoPages.files(); | |
| 58 | 68 | for (const c of [push("main"), push("other"), change("git.deleted", { ref: "refs/heads/old" }), change("status.created", { sha: "abc" }), change("release.published"), change("deployment_status.created")]) { | |
| ⋯ | |||
| 62 | 72 | assert.equal(files(change("comment.created", { number: 3 })), false); | |
| 63 | 73 | }); | |
| 64 | 74 | ||
| 75 | + | test("a page of files hears its own branch and its commit's checks, not every branch's", () => { | |
| 76 | + | const files = repoPages.files({ branch: "main", branches: ["main", "feature"], sha: "abc" }); | |
| 77 | + | assert.equal(files(push("main")), true, "its files and latest commit"); | |
| 78 | + | assert.equal(files(push("feature")), false, "another branch moving changes nothing here"); | |
| 79 | + | assert.equal(files(push("brand-new")), true, "a new branch: the menu and count"); | |
| 80 | + | assert.equal(files(change("git.deleted", { ref: "refs/heads/feature" })), true, "one of the menu's gone"); | |
| 81 | + | assert.equal(files(change("git.deleted", { ref: "refs/heads/never-listed" })), false); | |
| 82 | + | assert.equal(files(change("git.push", { ref: "refs/tags/v2" })), true, "the About's tags"); | |
| 83 | + | assert.equal(files(change("branch.renamed", { ref: "refs/heads/feature" })), true); | |
| 84 | + | assert.equal(files(change("repo.default_branch_changed")), true); | |
| 85 | + | assert.equal(files(change("status.created", { sha: "abc" })), true, "its latest commit's checks"); | |
| 86 | + | assert.equal(files(change("check_run.completed", { sha: "f00d" })), false, "another commit's checks"); | |
| 87 | + | assert.equal(files(change("run.updated", { run: "run_1" })), true, "a check that names no commit may be its"); | |
| 88 | + | assert.equal(files(change("deployment_status.created")), true); | |
| 89 | + | assert.equal(files(change("release.published")), true); | |
| 90 | + | // A tag or a commit shown: no branch of its own moves it. | |
| 91 | + | const tag = repoPages.files({ branch: null, branches: ["main"], sha: "abc" }); | |
| 92 | + | assert.equal(tag(push("main")), false); | |
| 93 | + | // Without the branches, any branch may be new: only its own and deletions count. | |
| 94 | + | const unknown = repoPages.files({ branch: "main", branches: null, sha: "abc" }); | |
| 95 | + | assert.equal(unknown(push("other")), false); | |
| 96 | + | assert.equal(unknown(change("git.deleted", { ref: "refs/heads/other" })), true); | |
| 97 | + | }); | |
| 98 | + | ||
| 65 | 99 | test("the commits page hears pushes to its own branch only", () => { | |
| 66 | 100 | const commits = repoPages.commits("main"); | |
| 67 | 101 | assert.equal(commits(push("main")), true); | |
| ⋯ | |||
| 69 | 103 | assert.equal(commits(change("git.push", { ref: "refs/tags/v1" })), false); | |
| 70 | 104 | assert.equal(commits(change("workflow.completed", { sha: "abc", run: "run_1" })), true, "checks on its commits"); | |
| 71 | 105 | assert.equal(commits(change("pull.opened", { number: 1 })), false); | |
| 106 | + | const shown = repoPages.commits("main", ["abc", "def"]); | |
| 107 | + | assert.equal(shown(change("status.created", { sha: "def" })), true, "checks on a commit it lists"); | |
| 108 | + | assert.equal(shown(change("status.created", { sha: "f00d" })), false, "not on one it does not"); | |
| 72 | 109 | }); | |
| 73 | 110 | ||
| 74 | 111 | test("branches, tags, and the lists of pull requests and issues", () => { | |
| ⋯ | |||
| 183 | 220 | assert.deepEqual(gate(due, { now: NOW, visible: true, busy: false, navigating: false }), { action: "run" }); | |
| 184 | 221 | }); | |
| 185 | 222 | ||
| 186 | − | test("a tab out of view refreshes at most once a minute", () => { | |
| 223 | + | test("a burst of changes refreshes at most every two seconds", () => { | |
| 224 | + | const idle = { now: NOW, visible: true, busy: false, navigating: false }; | |
| 225 | + | // Quiet for a second, but the page refreshed half a second ago: it waits out the gap. | |
| 226 | + | const justRan = { pendingSince: NOW - LIVE_DEBOUNCE_MS, lastChangeAt: NOW - LIVE_DEBOUNCE_MS, lastRunAt: NOW - 500 }; | |
| 227 | + | assert.deepEqual(gate(justRan, idle), { action: "wait", ms: LIVE_MIN_GAP_MS - 500 }); | |
| 228 | + | assert.deepEqual(gate(justRan, { ...idle, now: NOW - 500 + LIVE_MIN_GAP_MS }), { action: "run" }); | |
| 229 | + | // A steady stream after a refresh: one more at most five seconds on, never sooner than the gap. | |
| 230 | + | const stream = { pendingSince: NOW - LIVE_MAX_WAIT_MS, lastChangeAt: NOW, lastRunAt: NOW - LIVE_MAX_WAIT_MS - 10 }; | |
| 231 | + | assert.deepEqual(gate(stream, idle), { action: "run" }); | |
| 232 | + | // Changes arriving every 100 ms for a minute refresh no more than once per gap. | |
| 233 | + | let state = { pendingSince: null as number | null, lastChangeAt: null as number | null, lastRunAt: NOW }; | |
| 234 | + | let runs = 0; | |
| 235 | + | for (let t = NOW; t < NOW + 60_000; t += 100) { | |
| 236 | + | state = { ...state, pendingSince: state.pendingSince ?? t, lastChangeAt: t }; | |
| 237 | + | if (gate(state, { ...idle, now: t }).action === "run") { | |
| 238 | + | runs++; | |
| 239 | + | state = { pendingSince: null, lastChangeAt: null, lastRunAt: t }; | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | assert.ok(runs <= 60_000 / LIVE_MIN_GAP_MS, `${runs} runs`); | |
| 243 | + | assert.ok(runs >= 60_000 / LIVE_MAX_WAIT_MS - 1, `${runs} runs: still kept up to date`); | |
| 244 | + | }); | |
| 245 | + | ||
| 246 | + | test("a tab out of view never refreshes; back in view, it catches up at once", () => { | |
| 187 | 247 | const hidden = { now: NOW, visible: false, busy: false, navigating: false }; | |
| 188 | − | const ranLately = { pendingSince: NOW - 5000, lastChangeAt: NOW - 5000, lastRunAt: NOW - 20_000 }; | |
| 189 | − | assert.deepEqual(gate(ranLately, hidden), { action: "wait", ms: HIDDEN_MIN_MS - 20_000 }); | |
| 190 | − | assert.deepEqual(gate({ ...ranLately, lastRunAt: NOW - HIDDEN_MIN_MS }, hidden), { action: "run" }); | |
| 191 | − | // Back in view: at once. | |
| 192 | − | assert.deepEqual(gate(ranLately, { ...hidden, visible: true }), { action: "run" }); | |
| 248 | + | const waiting = { pendingSince: NOW - 5000, lastChangeAt: NOW - 5000, lastRunAt: NOW - 20 * 60_000 }; | |
| 249 | + | assert.deepEqual(gate(waiting, hidden), { action: "hold" }, "no timer: coming back asks again"); | |
| 250 | + | assert.deepEqual(gate({ pendingSince: null, lastChangeAt: null, lastRunAt: null }, hidden), { action: "idle" }); | |
| 251 | + | assert.deepEqual(gate(waiting, { ...hidden, visible: true }), { action: "run" }); | |
| 193 | 252 | }); | |
| 194 | 253 | ||
| 195 | 254 | test("without the socket: back in view every 30 s at most, a poll every minute in view, never with it", () => { | |
| 8 | 8 | * then every push, pull request, issue, comment, review, check, run, | |
| 9 | 9 | * release and security alert there arrives as a small `RepoChange`. A page | |
| 10 | 10 | * refreshes its loader when a change matches it, a second after the last | |
| 11 | − | * one, and never under someone typing or with a dialog open. Signed out, | |
| 12 | − | * or while the socket is down, it refreshes when the tab comes back into | |
| 13 | − | * view and once a minute while it is in view instead. | |
| 11 | + | * one, at most every two seconds, never under someone typing or with a | |
| 12 | + | * dialog open, and not at all while the tab is out of view: it catches up | |
| 13 | + | * once when it comes back. A busy repository sends changes all the time | |
| 14 | + | * (every branch's pushes, every commit's checks), so each page matches | |
| 15 | + | * only what it shows: the Code page its own branch and the checks of the | |
| 16 | + | * commit it shows, not another branch's. Signed out, or while the socket | |
| 17 | + | * is down, it refreshes when the tab comes back into view and once a | |
| 18 | + | * minute while it is in view instead. | |
| 14 | 19 | */ | |
| 15 | 20 | import type { RepoChange } from "@g1t/contracts"; | |
| 16 | 21 | ||
| ⋯ | |||
| 23 | 28 | export const LIVE_DEBOUNCE_MS = 1000; | |
| 24 | 29 | /** However steady the changes, a page waits no longer than this before refreshing. */ | |
| 25 | 30 | export const LIVE_MAX_WAIT_MS = 5000; | |
| 31 | + | /** However many changes arrive, a page refreshes at most this often. */ | |
| 32 | + | export const LIVE_MIN_GAP_MS = 2000; | |
| 26 | 33 | /** How often a page held back (someone typing, a dialog open) looks again. */ | |
| 27 | 34 | export const LIVE_HOLD_CHECK_MS = 1000; | |
| 28 | − | /** A tab out of view refreshes at most this often. */ | |
| 29 | − | export const HIDDEN_MIN_MS = 60_000; | |
| 30 | 35 | /** Without the socket: coming back into view refreshes at most this often. */ | |
| 31 | 36 | export const FALLBACK_FOCUS_MS = 30_000; | |
| 32 | 37 | /** Without the socket: how often a page in view refreshes. */ | |
| ⋯ | |||
| 73 | 78 | ||
| 74 | 79 | const ALERTS = ["secret_scanning_alert", "code_scanning_alert", "vulnerability_alert"]; | |
| 75 | 80 | ||
| 81 | + | /** A check on one of `shas`: any check when they are not known, and one that names no commit. */ | |
| 82 | + | function isCheckOn(change: RepoChange, shas: readonly string[] | null | undefined): boolean { | |
| 83 | + | return isCheck(change) && (shas == null || !change.sha || shas.includes(change.sha)); | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | /** What a page of files shows, for matching only what can change it. */ | |
| 87 | + | export type ShownFiles = { | |
| 88 | + | /** The branch shown; null for a tag or a commit. */ | |
| 89 | + | branch: string | null; | |
| 90 | + | /** Every branch, for the branch menu and count: a new one, or one of them deleted, changes them. */ | |
| 91 | + | branches?: readonly string[] | null; | |
| 92 | + | /** The commit shown, whose checks are on the page. */ | |
| 93 | + | sha?: string | null; | |
| 94 | + | }; | |
| 95 | + | ||
| 76 | 96 | /** What each kind of repository page shows. */ | |
| 77 | 97 | export const repoPages = { | |
| 78 | 98 | /** | |
| 79 | − | * Files and a folder: any ref moving (the files and latest | |
| 80 | − | * commit, the branch menu, recent pushes), checks on commits, releases | |
| 81 | − | * and deployments. | |
| 99 | + | * Files and a folder: its own branch moving (the files and the latest | |
| 100 | + | * commit), a branch made, deleted or renamed and the default changing | |
| 101 | + | * (the branch menu and count), any tag (the About's), checks on the | |
| 102 | + | * commit shown, releases and deployments. Without `shown`, any ref and | |
| 103 | + | * any check. | |
| 82 | 104 | */ | |
| 83 | − | files(): RepoMatch { | |
| 84 | − | return (change) => isRefChange(change) || isCheck(change) || ["release", "deployment", "deployment_status"].includes(family(change)); | |
| 105 | + | files(shown?: ShownFiles): RepoMatch { | |
| 106 | + | return (change) => { | |
| 107 | + | if (["release", "deployment", "deployment_status"].includes(family(change))) return true; | |
| 108 | + | if (!shown) return isRefChange(change) || isCheck(change); | |
| 109 | + | if (isCheck(change)) return isCheckOn(change, shown.sha ? [shown.sha] : null); | |
| 110 | + | if (!isRefChange(change)) return false; | |
| 111 | + | const branch = branchOf(change.ref); | |
| 112 | + | if (branch == null || change.kind === "branch.renamed" || change.kind === "repo.default_branch_changed") return true; | |
| 113 | + | if (branch === shown.branch) return true; | |
| 114 | + | // Another branch: only made or deleted matters, by the menu it is in or not. | |
| 115 | + | const known = shown.branches == null || shown.branches.includes(branch); | |
| 116 | + | return change.kind === "git.deleted" ? known : !known; | |
| 117 | + | }; | |
| 85 | 118 | }, | |
| 86 | 119 | /** | |
| 87 | 120 | * A project's overview: what it runs and how it is doing (deployments, | |
| 88 | 121 | * checks, releases), who is working on it (claims and sessions), and the | |
| 89 | 122 | * pull requests that may need someone (opened, readied, reviewed, merged). | |
| 123 | + | * Of the checks, only the shown commit's (`sha`) and finished pull | |
| 124 | + | * requests' (how often checks pass); without `sha`, all. | |
| 90 | 125 | */ | |
| 91 | − | overview(): RepoMatch { | |
| 126 | + | overview(sha?: string | null): RepoMatch { | |
| 92 | 127 | return (change) => | |
| 93 | − | isCheck(change) || | |
| 128 | + | (change.kind === "checks.completed" || isCheckOn(change, sha ? [sha] : null)) || | |
| 94 | 129 | ["release", "deployment", "deployment_status", "work", "pull", "review"].includes(family(change)) || | |
| 95 | 130 | SESSION_KINDS.includes(change.kind); | |
| 96 | 131 | }, | |
| 97 | − | /** Commits: pushes to the branch shown, and checks on commits. */ | |
| 98 | − | commits(branch: string | null): RepoMatch { | |
| 99 | − | return (change) => (branch == null ? change.kind === "git.push" && !tagOf(change.ref) : isPushTo(change, branch)) || isCheck(change); | |
| 132 | + | /** Commits: pushes to the branch shown, and checks on the commits shown (on any without `shas`). */ | |
| 133 | + | commits(branch: string | null, shas?: readonly string[] | null): RepoMatch { | |
| 134 | + | return (change) => (branch == null ? change.kind === "git.push" && !tagOf(change.ref) : isPushTo(change, branch)) || isCheckOn(change, shas); | |
| 100 | 135 | }, | |
| 101 | 136 | /** Branches and tags: any ref moving, pull requests (a branch's), checks. */ | |
| 102 | 137 | refs(): RepoMatch { | |
| ⋯ | |||
| 196 | 231 | navigating: boolean; | |
| 197 | 232 | }; | |
| 198 | 233 | ||
| 199 | − | export type GateDecision = { action: "idle" } | { action: "run" } | { action: "wait"; ms: number }; | |
| 234 | + | /** | |
| 235 | + | * `hold`: nothing to do until the tab comes back into view, which asks | |
| 236 | + | * again; no timer runs meanwhile. | |
| 237 | + | */ | |
| 238 | + | export type GateDecision = { action: "idle" } | { action: "run" } | { action: "hold" } | { action: "wait"; ms: number }; | |
| 200 | 239 | ||
| 201 | 240 | /** | |
| 202 | 241 | * Whether to refresh now for the changes waiting, or how long to wait | |
| 203 | 242 | * before asking again. A second after the last change (at most five after | |
| 204 | − | * the first), never under someone typing or with a dialog open, and at | |
| 205 | − | * most once a minute for a tab out of view, which refreshes as soon as it | |
| 206 | − | * comes back instead. | |
| 243 | + | * the first), at most every two seconds, never under someone typing or | |
| 244 | + | * with a dialog open, and never for a tab out of view: what waited is | |
| 245 | + | * fetched once, as soon as it comes back. | |
| 207 | 246 | */ | |
| 208 | 247 | export function gate(state: GateState, input: GateInput): GateDecision { | |
| 209 | 248 | if (state.pendingSince == null) return { action: "idle" }; | |
| 249 | + | if (!input.visible) return { action: "hold" }; | |
| 210 | 250 | if (input.busy || input.navigating) return { action: "wait", ms: LIVE_HOLD_CHECK_MS }; | |
| 211 | − | if (!input.visible && state.lastRunAt != null && input.now - state.lastRunAt < HIDDEN_MIN_MS) { | |
| 212 | − | return { action: "wait", ms: HIDDEN_MIN_MS - (input.now - state.lastRunAt) }; | |
| 213 | − | } | |
| 214 | 251 | const quiet = input.now - (state.lastChangeAt ?? state.pendingSince); | |
| 215 | 252 | const waited = input.now - state.pendingSince; | |
| 216 | − | if (quiet < LIVE_DEBOUNCE_MS && waited < LIVE_MAX_WAIT_MS) { | |
| 217 | − | return { action: "wait", ms: Math.min(LIVE_DEBOUNCE_MS - quiet, LIVE_MAX_WAIT_MS - waited) }; | |
| 218 | − | } | |
| 219 | − | return { action: "run" }; | |
| 253 | + | const since = state.lastRunAt == null ? Infinity : input.now - state.lastRunAt; | |
| 254 | + | const ms = Math.max(quiet < LIVE_DEBOUNCE_MS && waited < LIVE_MAX_WAIT_MS ? Math.min(LIVE_DEBOUNCE_MS - quiet, LIVE_MAX_WAIT_MS - waited) : 0, LIVE_MIN_GAP_MS - since); | |
| 255 | + | return ms > 0 ? { action: "wait", ms } : { action: "run" }; | |
| 220 | 256 | } | |
| 221 | 257 | ||
| 222 | 258 | /** | |
| 6 | 6 | * Signed in, the tab watches the repository over its live socket: the | |
| 7 | 7 | * site checks the person may read it (`/-/notify`, `watch_repo`), then | |
| 8 | 8 | * every change there arrives as it happens, and the page's loader runs | |
| 9 | − | * again a second after the last matching one. Never under someone typing | |
| 10 | − | * on the page or with a dialog open (it waits until they are done), and | |
| 11 | − | * at most once a minute for a tab out of view. Signed out, or while the | |
| 9 | + | * again a second after the last matching one, at most every two seconds. | |
| 10 | + | * Never under someone typing on the page or with a dialog open (it waits | |
| 11 | + | * until they are done), and never for a tab out of view: it catches up | |
| 12 | + | * once when the tab comes back. Signed out, or while the | |
| 12 | 13 | * socket is down, the page refreshes when the tab comes back into view | |
| 13 | 14 | * (at most every 30 s) and once a minute while it is in view. | |
| 14 | 15 | * | |
| ⋯ | |||
| 147 | 148 | const decision = gate(state.current, { now: Date.now(), visible: visibleNow(), busy: busyNow(), navigating: latest.current.navigating }); | |
| 148 | 149 | if (decision.action === "run") run(); | |
| 149 | 150 | else if (decision.action === "wait") timer.current = setTimeout(check, decision.ms); | |
| 151 | + | // "hold": out of view; coming back into view (`back` below) asks again. | |
| 150 | 152 | }, [run]); | |
| 151 | 153 | ||
| 152 | 154 | /** Something to refresh for: `now` skips the second's wait for more. */ | |
| 41 | 41 | ||
| 42 | 42 | export default function Code({ loaderData }: Route.ComponentProps) { | |
| 43 | 43 | const project = useProject(); | |
| 44 | − | // The files and latest commit follow pushes; checks update as they report. | |
| 45 | − | useRepoLive({ match: repoPages.files() }); | |
| 44 | + | // The files and latest commit follow pushes to the branch; the latest | |
| 45 | + | // commit's checks update as they report. | |
| 46 | + | const { tree, branches } = loaderData; | |
| 47 | + | useRepoLive({ match: repoPages.files({ branch: tree.ref, branches: branches?.map((branch) => branch.name), sha: tree.head?.hash }) }); | |
| 46 | 48 | return ( | |
| 47 | 49 | <TreeView | |
| 48 | 50 | tree={loaderData.tree} |
| 64 | 64 | // moving the list under the reader; checks on the shown ones update. | |
| 65 | 65 | const branch = useProject()?.repo.defaultBranch ?? null; | |
| 66 | 66 | const pushed = (change: Parameters<typeof isPushTo>[0]) => change.kind === "git.push" && isPushTo(change, branch); | |
| 67 | − | const { held, show } = useRepoLive({ match: repoPages.commits(branch), hold: commits.length > 0 ? pushed : undefined }); | |
| 67 | + | const { held, show } = useRepoLive({ match: repoPages.commits(branch, commits.map((commit) => commit.hash)), hold: commits.length > 0 ? pushed : undefined }); | |
| 68 | 68 | if (commits.length === 0) { | |
| 69 | 69 | return <EmptyState title="No commits yet" />; | |
| 70 | 70 | } |
| 819 | 819 | const elsewhereUrl = loaderData.deployment?.url ?? project?.productionUrl ?? null; | |
| 820 | 820 | const working = claims.length + sessions.length > 0; | |
| 821 | 821 | // Deployments, checks, releases, claims and sessions show as they happen. | |
| 822 | − | useRepoLive({ match: repoPages.overview() }); | |
| 822 | + | useRepoLive({ match: repoPages.overview(loaderData.commit?.hash) }); | |
| 823 | 823 | // Claims lapse and sessions go idle with no event to say so. | |
| 824 | 824 | useRefreshWhile(working, 15_000); | |
| 825 | 825 | const live = liveByIssue(claims, sessions); |
| 45 | 45 | ||
| 46 | 46 | export default function Tree({ loaderData }: Route.ComponentProps) { | |
| 47 | 47 | const project = useProject(); | |
| 48 | − | // The files and latest commit follow pushes; checks update as they report. | |
| 49 | − | useRepoLive({ match: repoPages.files() }); | |
| 48 | + | // The files and latest commit follow pushes to the branch (none for a | |
| 49 | + | // tag or a commit); the latest commit's checks update as they report. | |
| 50 | + | const { tree, branches } = loaderData; | |
| 51 | + | const names = branches?.map((branch) => branch.name); | |
| 52 | + | useRepoLive({ match: repoPages.files({ branch: !names || names.includes(tree.ref) ? tree.ref : null, branches: names, sha: tree.head?.hash }) }); | |
| 50 | 53 | return ( | |
| 51 | 54 | <TreeView | |
| 52 | 55 | tree={loaderData.tree} |
Comments
Sign in to comment.