Public: anyone can read this; the crew can change it.
GET /live/feed names its Server-Timing phases: session, feed, render #3761
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3759-get-live-feed-names-its-server-timing-ph → main 1 commit fork
2 files
changed +88
−3
src/web/studio/feed.ts +19 −3
| @@ -12,6 +12,7 @@ import { chipsOf, KIND_CHIPS, type KindChip } from "../../core/feed_chips.ts"; | ||
| 12 | 12 | import { marksByName, type Mark } from "../../core/marks.ts"; |
| 13 | 13 | import { getUsersByIds, type User } from "../../core/users.ts"; |
| 14 | 14 | import type { Ctx } from "../../http/context.ts"; |
| 15 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; | |
| 15 | 16 | import { placementsOf, type Placement } from "../../core/placements.ts"; |
| 16 | 17 | import { recentThoughts, type Thought } from "../../core/thoughts.ts"; |
| 17 | 18 | import { readFeed } from "../../feed/feed.ts"; |
| @@ -230,8 +231,8 @@ function chipsBar(user: User, ctx: Ctx, items: readonly FeedItem[]): Html { | ||
| 230 | 231 | </div>`; |
| 231 | 232 | } |
| 232 | 233 | |
| 233 | /** The region: cards, or one line saying there is nothing to do. */ | |
| 234 | export function feedRegion(user: User, ctx: Ctx): Html { | |
| 234 | /** What the region shows, read: the posts, the thoughts, where they sit, who posted. */ | |
| 235 | function readRegion(user: User, ctx: Ctx) { | |
| 235 | 236 | // What needs you is pinned above the timeline (`needsYouSection`), so an |
| 236 | 237 | // ask is never a second time a post below it; the feed API keeps them. |
| 237 | 238 | const items = readFeed(ctx.app.db, user, { limit: FEED_FIRST_PAGE, withoutAsks: true }).items; |
| @@ -242,6 +243,16 @@ export function feedRegion(user: User, ctx: Ctx): Html { | ||
| 242 | 243 | ctx.app.db, |
| 243 | 244 | items.map((item) => item.actor ?? ""), |
| 244 | 245 | ); |
| 246 | return { items, thoughts, placed, marks }; | |
| 247 | } | |
| 248 | ||
| 249 | /** The region: cards, or one line saying there is nothing to do. */ | |
| 250 | export function feedRegion(user: User, ctx: Ctx): Html { | |
| 251 | return drawRegion(user, ctx, readRegion(user, ctx)); | |
| 252 | } | |
| 253 | ||
| 254 | function drawRegion(user: User, ctx: Ctx, read: ReturnType<typeof readRegion>): Html { | |
| 255 | const { items, thoughts, placed, marks } = read; | |
| 245 | 256 | return html`<section |
| 246 | 257 | id="live-feed" |
| 247 | 258 | class="feed" |
| @@ -264,8 +275,13 @@ export function feedRegion(user: User, ctx: Ctx): Html { | ||
| 264 | 275 | // GET /live/feed — the feed alone, for the region that shows it. |
| 265 | 276 | export function liveFeedFragment(ctx: Ctx): Response { |
| 266 | 277 | const user = ctx.auth.user; |
| 278 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 267 | 279 | if (!user) return fragment(html``, { status: 404 }); |
| 268 | return fragment(feedRegion(user, ctx), { request: ctx.request }); | |
| 280 | const read = timePhase(ctx.request, "feed", () => readRegion(user, ctx)); | |
| 281 | const rendering = performance.now(); | |
| 282 | const response = fragment(drawRegion(user, ctx, read), { request: ctx.request }); | |
| 283 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 284 | return response; | |
| 269 | 285 | } |
| 270 | 286 | |
| 271 | 287 | /** |
test/live-feed-timing.test.ts added +69
| @@ -0,0 +1,69 @@ | ||
| 1 | /** `/live/feed` names session, feed and render in Server-Timing (#3403). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createRepo } from "../src/actions/repos.ts"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 10 | import { proveAll } from "./helpers/verified.ts"; | |
| 11 | import { quietLogs } from "./helpers/globals.ts"; | |
| 12 | ||
| 13 | quietLogs(); | |
| 14 | ||
| 15 | let root = ""; | |
| 16 | let app: App; | |
| 17 | let servers: Servers; | |
| 18 | ||
| 19 | beforeAll(async () => { | |
| 20 | root = mkdtempSync(join(tmpdir(), "forge-live-feed-timing-")); | |
| 21 | app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" }); | |
| 22 | const u = await createUser(app.db, { | |
| 23 | origin: "seed", | |
| 24 | trusted: true, | |
| 25 | username: "alice", | |
| 26 | email: "[email protected]", | |
| 27 | password: "a-good-passphrase", | |
| 28 | adminIfFirst: true, | |
| 29 | }); | |
| 30 | if (!u.ok) throw new Error(u.error); | |
| 31 | proveAll(app); | |
| 32 | const made = await createRepo(app, { id: u.value.id, username: "alice" } as never, { name: "forge" }); | |
| 33 | if (!made.ok) throw new Error(made.error); | |
| 34 | servers = await serve(app); | |
| 35 | }); | |
| 36 | afterAll(() => { | |
| 37 | servers.stop(); | |
| 38 | app.close(); | |
| 39 | rmSync(root, { recursive: true, force: true }); | |
| 40 | }); | |
| 41 | ||
| 42 | const signIn = async () => { | |
| 43 | const signin = await fetch(`${servers.url}/signin`, { | |
| 44 | method: "POST", | |
| 45 | headers: { origin: servers.url }, | |
| 46 | body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), | |
| 47 | redirect: "manual", | |
| 48 | }); | |
| 49 | return (signin.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 50 | }; | |
| 51 | const names = (res: Response) => | |
| 52 | (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]); | |
| 53 | ||
| 54 | test("GET /live/feed names its phases", async () => { | |
| 55 | const res = await fetch(`${servers.url}/live/feed`, { headers: { cookie: await signIn() } }); | |
| 56 | expect(res.status).toBe(200); | |
| 57 | expect(await res.text()).toContain('id="live-feed"'); | |
| 58 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 59 | for (const name of ["app", "session", "feed", "render"]) | |
| 60 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 61 | }); | |
| 62 | ||
| 63 | test("signed out, /live/feed is a 404 fragment that still names session", async () => { | |
| 64 | const res = await fetch(`${servers.url}/live/feed`); | |
| 65 | expect(res.status).toBe(404); | |
| 66 | expect(await res.text()).toBe(""); | |
| 67 | expect(names(res)).toContain("session"); | |
| 68 | expect(names(res)).not.toContain("feed"); | |
| 69 | }); |
new above
offline