Public: anyone can read this; the crew can change it.
The new forge page's Server-Timing names its phases: session, each read, render #3471
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3460-the-new-forge-page-s-server-timing-names → main 1 commit fork 2 threads · 2 unresolved
2 files
changed +79
−9
src/web/pages/new-forge.ts +34 −9
| @@ -13,6 +13,7 @@ import { listAgents } from "../../core/agents.ts"; | ||
| 13 | 13 | import { listReposVisibleTo } from "../../core/repos.ts"; |
| 14 | 14 | import type { User } from "../../core/users.ts"; |
| 15 | 15 | import type { Ctx } from "../../http/context.ts"; |
| 16 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; | |
| 16 | 17 | import { childHost, childUrlOf, noRoom, spawnDoor, type Door } from "../../spawn/door.ts"; |
| 17 | 18 | import { |
| 18 | 19 | LEFT_BEHIND, |
| @@ -94,14 +95,37 @@ function checks( | ||
| 94 | 95 | )}`; |
| 95 | 96 | } |
| 96 | 97 | |
| 98 | /** What the form reads before it draws: room, then (when there is room) the repositories and the crew. */ | |
| 99 | interface Reads { | |
| 100 | readonly full: string | undefined; | |
| 101 | readonly repos: ReturnType<typeof listReposVisibleTo>; | |
| 102 | readonly crew: ReturnType<typeof listAgents>; | |
| 103 | } | |
| 104 | ||
| 105 | function readsFor(ctx: Ctx, user: User | undefined): Reads { | |
| 106 | const full = timePhase(ctx.request, "room", () => noRoom(ctx.app)); | |
| 107 | if (full) return { full, repos: [], crew: [] }; | |
| 108 | // A stranger sees only public repositories, and no crew: nothing private | |
| 109 | // travels to a child they have not claimed. | |
| 110 | const repos = timePhase(ctx.request, "repos", () => | |
| 111 | listReposVisibleTo(ctx.app.db, user, { limit: 200 }), | |
| 112 | ); | |
| 113 | const crew = | |
| 114 | user === undefined | |
| 115 | ? [] | |
| 116 | : timePhase(ctx.request, "crew", () => listAgents(ctx.app.db)); | |
| 117 | return { full, repos, crew }; | |
| 118 | } | |
| 119 | ||
| 97 | 120 | function formView( |
| 98 | 121 | ctx: Ctx, |
| 99 | 122 | user: User | undefined, |
| 100 | 123 | values: Values, |
| 101 | 124 | errors: Errors, |
| 102 | 125 | plan: Html | null, |
| 126 | reads: Reads, | |
| 103 | 127 | ): Html { |
| 104 | const full = noRoom(ctx.app); | |
| 128 | const { full, repos, crew } = reads; | |
| 105 | 129 | if (full) |
| 106 | 130 | return html`<div class="narrow-wide"> |
| 107 | 131 | ${pageHead("New forge")} |
| @@ -112,11 +136,7 @@ function formView( | ||
| 112 | 136 | : null}`, |
| 113 | 137 | )} |
| 114 | 138 | </div>`; |
| 115 | // A stranger sees only public repositories, and no crew: nothing private | |
| 116 | // travels to a child they have not claimed. | |
| 117 | 139 | const anon = user === undefined; |
| 118 | const repos = listReposVisibleTo(ctx.app.db, user, { limit: 200 }); | |
| 119 | const crew = anon ? [] : listAgents(ctx.app.db); | |
| 120 | 140 | return html`<div class="narrow-wide"> |
| 121 | 141 | ${pageHead("New forge", { |
| 122 | 142 | sub: anon |
| @@ -369,11 +389,16 @@ const asBool = (form: Form, name: string): boolean => | ||
| 369 | 389 | export function newForgePage(ctx: Ctx): Response { |
| 370 | 390 | const user = ctx.auth.user; |
| 371 | 391 | if (user && user.kind !== "user") return notFoundPage(ctx); |
| 372 | return page({ | |
| 392 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 393 | const reads = readsFor(ctx, user); | |
| 394 | const rendering = performance.now(); | |
| 395 | const response = page({ | |
| 373 | 396 | ctx, |
| 374 | 397 | title: "New forge", |
| 375 | body: formView(ctx, user, EMPTY, {}, null), | |
| 398 | body: formView(ctx, user, EMPTY, {}, null, reads), | |
| 376 | 399 | }); |
| 400 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 401 | return response; | |
| 377 | 402 | } |
| 378 | 403 | |
| 379 | 404 | // POST /forges/new — `do=plan` shows the plan; anything else spawns. |
| @@ -417,7 +442,7 @@ export async function newForgeSubmit(ctx: Ctx): Promise<Response> { | ||
| 417 | 442 | { |
| 418 | 443 | ctx, |
| 419 | 444 | title: "New forge", |
| 420 | body: formView(ctx, user, values, errorsOf(door), null), | |
| 445 | body: formView(ctx, user, values, errorsOf(door), null, readsFor(ctx, user)), | |
| 421 | 446 | }, |
| 422 | 447 | door.status === 422 ? 422 : door.status, |
| 423 | 448 | ); |
| @@ -425,7 +450,7 @@ export async function newForgeSubmit(ctx: Ctx): Promise<Response> { | ||
| 425 | 450 | return page({ |
| 426 | 451 | ctx, |
| 427 | 452 | title: "New forge", |
| 428 | body: formView(ctx, user, values, {}, planView(ctx, user, values, door.body)), | |
| 453 | body: formView(ctx, user, values, {}, planView(ctx, user, values, door.body), readsFor(ctx, user)), | |
| 429 | 454 | }); |
| 430 | 455 | const response = page( |
| 431 | 456 | { ctx, title: "New forge", body: madeView(door.body) }, |
test/new-forge-timing.test.ts added +45
| @@ -0,0 +1,45 @@ | ||
| 1 | /** /forges/new's Server-Timing names its phases: session, room, repos, crew, render (#3460). */ | |
Issue number reads #3460; this pull is #3471. Add an anonymous request asserting room and repos but no crew. |
||
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { createToken } from "../src/core/tokens.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { serve } from "../src/http/server.ts"; | |
| 10 | ||
| 11 | let dir = ""; | |
| 12 | let app: ReturnType<typeof createApp>; | |
| 13 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 14 | let bearer = ""; | |
| 15 | ||
| 16 | beforeAll(async () => { | |
| 17 | dir = await mkdtemp(join(tmpdir(), "forge-new-forge-timing-")); | |
| 18 | app = createApp({ dataDir: dir, port: 0 }); | |
| 19 | servers = await serve(app); | |
| 20 | const made = await createUser(app.db, { | |
| 21 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", | |
| 22 | password: "a-good-passphrase", adminIfFirst: true, | |
| 23 | }); | |
| 24 | if (!made.ok) throw new Error(made.error); | |
| 25 | bearer = createToken(app.db, made.value.id, "t", "admin").token; | |
| 26 | }); | |
| 27 | ||
| 28 | afterAll(async () => { | |
| 29 | servers.stop(); | |
| 30 | app.close(); | |
| 31 | await rm(dir, { recursive: true, force: true }); | |
| 32 | }); | |
| 33 | ||
| 34 | test("the new forge page names session, room, repos, crew and render once each", async () => { | |
| 35 | const res = await fetch(servers.url + "/forges/new", { headers: { authorization: `Bearer ${bearer}` } }); | |
| 36 | expect(res.status).toBe(200); | |
| 37 | expect(await res.text()).toContain("New forge"); | |
| 38 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 39 | const names = parts.map((p) => p.split(";")[0]!); | |
| 40 | for (const n of ["session", "room", "repos", "crew", "render"]) { | |
| 41 | expect(names.filter((x) => x === n).length).toBe(1); | |
| 42 | const part = parts.find((p) => p.startsWith(`${n};`))!; | |
| 43 | expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN(); | |
| 44 | } | |
| 45 | }); |
new above
offline
This line is long and the plan re-render has no session/render phases, unlike GET. Wrap it, and either time it too or leave it deliberately.