Public: anyone can read this; the crew can change it.
realtime #16
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6-realtime → main 1 commit fork
5 files
changed +88
−5
src/web/pages/branches.ts +4 −1
| @@ -26,6 +26,7 @@ import { loadRepo, notFoundPage, scopeHeader, signInFirst } from "../chrome.ts"; | ||
| 26 | 26 | import { field as formValue, readForm } from "../form.ts"; |
| 27 | 27 | import { html, when, type Html } from "../html.ts"; |
| 28 | 28 | import { flashTo, page } from "../layout.ts"; |
| 29 | import { liveClientJs, liveMount } from "../live.ts"; | |
| 29 | 30 | import { localScope } from "../scope.ts"; |
| 30 | 31 | import { |
| 31 | 32 | badge, |
| @@ -277,7 +278,8 @@ export async function renderBranches( | ||
| 277 | 278 | )}`, |
| 278 | 279 | }, |
| 279 | 280 | )} |
| 280 | </div>`; | |
| 281 | </div> | |
| 282 | ${when(scope.kind === "local", () => liveMount({ repo: scope.base }))}`; | |
| 281 | 283 | |
| 282 | 284 | return page({ |
| 283 | 285 | ctx, |
| @@ -285,6 +287,7 @@ export async function renderBranches( | ||
| 285 | 287 | crumbs: scope.crumbs, |
| 286 | 288 | palette: scope.palette, |
| 287 | 289 | body, |
| 290 | ...(scope.kind === "local" ? { scriptJs: liveClientJs } : {}), | |
| 288 | 291 | }); |
| 289 | 292 | } |
| 290 | 293 |
src/web/pages/commits.ts +6 −2
| @@ -21,6 +21,7 @@ import { diffSyntax } from "../diff-syntax.ts"; | ||
| 21 | 21 | import { renderDiff, type SyntaxMap } from "../diff-view.ts"; |
| 22 | 22 | import { html, when, type Html } from "../html.ts"; |
| 23 | 23 | import { page } from "../layout.ts"; |
| 24 | import { liveClientJs, liveMount } from "../live.ts"; | |
| 24 | 25 | import { localScope, type RepoScope } from "../scope.ts"; |
| 25 | 26 | import { |
| 26 | 27 | avatar, |
| @@ -210,7 +211,8 @@ export async function commitsPage(ctx: Ctx): Promise<Response> { | ||
| 210 | 211 | crumbs: scope.crumbs, |
| 211 | 212 | palette: scope.palette, |
| 212 | 213 | body: html`${scopeHeader(ctx, scope, "commits")}${switcher} |
| 213 | ${noCommits(num > 1)}`, | |
| 214 | ${noCommits(num > 1)} ${liveMount({ repo: base })}`, | |
| 215 | scriptJs: liveClientJs, | |
| 214 | 216 | }); |
| 215 | 217 | } |
| 216 | 218 | |
| @@ -223,13 +225,15 @@ export async function commitsPage(ctx: Ctx): Promise<Response> { | ||
| 223 | 225 | ...(num > 1 ? { prev: query(num - 1) } : {}), |
| 224 | 226 | ...(hasMore ? { next: query(num + 1) } : {}), |
| 225 | 227 | note: `Page ${num}`, |
| 226 | })}`; | |
| 228 | })} | |
| 229 | ${liveMount({ repo: base })}`; | |
| 227 | 230 | return page({ |
| 228 | 231 | ctx, |
| 229 | 232 | title: `Commits · ${repo.name}`, |
| 230 | 233 | crumbs: scope.crumbs, |
| 231 | 234 | palette: scope.palette, |
| 232 | 235 | body, |
| 236 | scriptJs: liveClientJs, | |
| 233 | 237 | }); |
| 234 | 238 | } |
| 235 | 239 |
src/web/pages/repo.ts +3 −2
| @@ -36,6 +36,7 @@ import { | ||
| 36 | 36 | } from "../chrome.ts"; |
| 37 | 37 | import { codeJs } from "../client.ts"; |
| 38 | 38 | import { highlightBlob } from "../code.ts"; |
| 39 | import { liveClientJs, liveMount } from "../live.ts"; | |
| 39 | 40 | import { appStrip } from "./apps.ts"; |
| 40 | 41 | import { studioApp } from "./studio.ts"; |
| 41 | 42 | import { getAppByRepo } from "../../core/apps.ts"; |
| @@ -916,14 +917,14 @@ export async function repoHome(ctx: Ctx): Promise<Response> { | ||
| 916 | 917 | entries: tree, |
| 917 | 918 | rail: aboutCard(ctx, repo, refs, drift), |
| 918 | 919 | })} |
| 919 | ${codeMount(scope, ref)} ${finderDialog()}`; | |
| 920 | ${codeMount(scope, ref)} ${finderDialog()} ${liveMount({ repo: scope.base })}`; | |
| 920 | 921 | return page({ |
| 921 | 922 | ctx, |
| 922 | 923 | title: repo.name, |
| 923 | 924 | crumbs: scope.crumbs, |
| 924 | 925 | palette: scope.palette, |
| 925 | 926 | body, |
| 926 | scriptJs: codeJs, | |
| 927 | scriptJs: `${codeJs}\n${liveClientJs}`, | |
| 927 | 928 | }); |
| 928 | 929 | } |
| 929 | 930 |
test/live-pages.test.ts added +73
| @@ -0,0 +1,73 @@ | ||
| 1 | /** | |
| 2 | * The browsing pages — repo home, commit history, branches — wire up the | |
| 3 | * same live client the issue/pull threads already had, so a push or a new | |
| 4 | * branch shows up as a "new activity" pill without a reload. Verifies the | |
| 5 | * wiring is present on a local repo and absent where there is nothing to | |
| 6 | * subscribe to (an empty repo, or a repo read live off another forge). | |
| 7 | */ | |
| 8 | ||
| 9 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 10 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 11 | import { tmpdir } from "node:os"; | |
| 12 | import { join } from "node:path"; | |
| 13 | import { editFiles } from "../src/actions/contents.ts"; | |
| 14 | import { createApp, type App } from "../src/app.ts"; | |
| 15 | import { createRepo } from "../src/core/repos.ts"; | |
| 16 | import { createUser } from "../src/core/users.ts"; | |
| 17 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 18 | ||
| 19 | describe("live browsing pages", () => { | |
| 20 | let dataDir: string; | |
| 21 | let app: App; | |
| 22 | let servers: Servers; | |
| 23 | let base: string; | |
| 24 | ||
| 25 | beforeAll(async () => { | |
| 26 | dataDir = await mkdtemp(join(tmpdir(), "forge-live-pages-")); | |
| 27 | app = createApp({ dataDir, port: 0 }); | |
| 28 | servers = await serve(app); | |
| 29 | base = servers.url; | |
| 30 | const user = await createUser(app.db, { | |
| 31 | username: "lex", | |
| 32 | email: "[email protected]", | |
| 33 | password: "live-pages-pass", | |
| 34 | }); | |
| 35 | if (!user.ok) throw new Error("user"); | |
| 36 | const repo = await createRepo(app.db, app.reposRoot, { | |
| 37 | ownerId: user.value.id, | |
| 38 | name: "proj", | |
| 39 | visibility: "public", | |
| 40 | }); | |
| 41 | if (!repo.ok) throw new Error("repo"); | |
| 42 | const committed = await editFiles(app, user.value, repo.value, { | |
| 43 | files: [{ path: "README.md", content: "# proj\n" }], | |
| 44 | message: "first commit", | |
| 45 | }); | |
| 46 | if (!committed.ok) throw new Error("commit"); | |
| 47 | }); | |
| 48 | ||
| 49 | afterAll(async () => { | |
| 50 | servers.stop(); | |
| 51 | app.close(); | |
| 52 | await rm(dataDir, { recursive: true, force: true }); | |
| 53 | }); | |
| 54 | ||
| 55 | test("the repo home wires up the live client", async () => { | |
| 56 | const html = await (await fetch(`${base}/lex/proj`)).text(); | |
| 57 | expect(html).toContain('id="live"'); | |
| 58 | expect(html).toContain('data-repo="/lex/proj"'); | |
| 59 | expect(html).toContain("new EventSource"); | |
| 60 | }); | |
| 61 | ||
| 62 | test("the commit history wires up the live client", async () => { | |
| 63 | const html = await (await fetch(`${base}/lex/proj/commits`)).text(); | |
| 64 | expect(html).toContain('id="live"'); | |
| 65 | expect(html).toContain("new EventSource"); | |
| 66 | }); | |
| 67 | ||
| 68 | test("the branches page wires up the live client", async () => { | |
| 69 | const html = await (await fetch(`${base}/lex/proj/branches`)).text(); | |
| 70 | expect(html).toContain('id="live"'); | |
| 71 | expect(html).toContain("new EventSource"); | |
| 72 | }); | |
| 73 | }); |
test/web-browse.test.ts +2
| @@ -365,6 +365,8 @@ describe("browsing an origin repository on the web", () => { | ||
| 365 | 365 | expect(html).not.toContain("/branches/main/delete"); |
| 366 | 366 | // The banner still says where the bytes came from. |
| 367 | 367 | expect(html).toContain("Reading live from"); |
| 368 | // A live-browse origin has no local repo to open an SSE stream against. | |
| 369 | expect(html).not.toContain("new EventSource"); | |
| 368 | 370 | }); |
| 369 | 371 | |
| 370 | 372 | test("commits and one commit render live, with the origin's author", async () => { |
new above
offline