Public: anyone can read this; the crew can change it.
The signed-out front page's shelf is read under 10 ms #6133
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6126-the-signed-out-front-page-s-shelf-is-rea → main 2 commits fork 3 threads · 3 unresolved
2 files
changed +136
−0
src/actions/front.ts +31
| @@ -150,10 +150,41 @@ function scoresFor(app: App, repoIds: readonly string[], now: number): Scores { | ||
| 150 | 150 | /** Forget the remembered ranking: the picks changed. */ |
| 151 | 151 | export function forgetFrontScores(app: App): void { |
| 152 | 152 | remembered.delete(app); |
| 153 | built.delete(app); | |
| 154 | } | |
| 155 | ||
| 156 | /** How long a built page is kept when nothing in the database changed: an app's state is not in it. */ | |
| 157 | const PAGE_MS = 3_000; | |
| 158 | ||
| 159 | /** | |
| 160 | * The built page, kept for `PAGE_MS` while the database is untouched: any | |
| 161 | * write on this connection, or by another, changes the key, so a repository | |
| 162 | * made private, a pick, a picture or a suspension shows on the next request. | |
| 163 | */ | |
| 164 | const built = new WeakMap<App, { key: string; at: number; page: FrontPage }>(); | |
| 165 | ||
| 166 | function dbKey(app: App): string { | |
| 167 | const row = app.db | |
| 168 | .query<{ c: number; v: number }, []>( | |
| 169 | "SELECT total_changes() AS c, (SELECT data_version FROM pragma_data_version) AS v", | |
| 170 | ) | |
| 171 | .get(); | |
| 172 | return `${row?.c}:${row?.v}`; | |
| 153 | 173 | } |
| 154 | 174 | |
| 155 | 175 | /** The front page as a stranger is shown it. */ |
| 156 | 176 | export function frontPage(app: App, now = Date.now()): FrontPage { |
| 177 | const key = dbKey(app); | |
| 178 | const kept = built.get(app); | |
| 179 | const wall = performance.now(); | |
| 180 | if (kept !== undefined && kept.key === key && wall - kept.at < PAGE_MS) | |
| 181 | return kept.page; | |
| 182 | const page = buildFrontPage(app, now); | |
| 183 | built.set(app, { key, at: wall, page }); | |
| 184 | return page; | |
| 185 | } | |
| 186 | ||
| 187 | function buildFrontPage(app: App, now: number): FrontPage { | |
| 157 | 188 | const db = app.db; |
| 158 | 189 | const picks = frontPicks(db); |
| 159 | 190 | const records = listPublicApps(db, READ); |
test/front-speed.test.ts added +105
| @@ -0,0 +1,105 @@ | ||
| 1 | /** The signed-out front page is kept between requests, and any write or a forgotten ranking drops it (#6126). */ | |
| 2 | import { afterAll, beforeAll, expect, spyOn, test } from "bun:test"; | |
| 3 | import { mkdirSync, writeFileSync } from "node:fs"; | |
| 4 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 5 | import { tmpdir } from "node:os"; | |
| 6 | import { join } from "node:path"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { forgetFrontScores, frontPage } from "../src/actions/front.ts"; | |
| 9 | import { createRepo } from "../src/actions/repos.ts"; | |
| 10 | import { AppsRuntime, type EnvStatus } from "../src/apps/runtime.ts"; | |
| 11 | import { createApp as insertApp, type AppRecord } from "../src/core/apps.ts"; | |
| 12 | import { recordLook } from "../src/core/looks.ts"; | |
| 13 | import { encodePng } from "../src/design/image/png.ts"; | |
| 14 | import { createUser, type User } from "../src/core/users.ts"; | |
| 15 | import { quietLogs } from "./helpers/globals.ts"; | |
| 16 | import { promote } from "./helpers/members.ts"; | |
| 17 | ||
| 18 | quietLogs(); | |
| 19 | ||
| 20 | const HEAD = "a".repeat(40); | |
| 21 | const PNG = Buffer.from(encodePng(1, 1, new Uint8Array([255, 255, 255, 255]))); | |
| 22 | const APPS = Number(process.env.N ?? 30); | |
| 23 | ||
| 24 | let dataDir = ""; | |
| 25 | let app: App; | |
| 26 | let runtime: AppsRuntime; | |
| 27 | let restore: () => void; | |
| 28 | const repoIds: string[] = []; | |
| 29 | ||
| 30 | beforeAll(async () => { | |
| 31 | dataDir = await mkdtemp(join(tmpdir(), "forge-front-speed-")); | |
| 32 | app = createApp({ dataDir, port: 0 }); | |
| 33 | const made = await createUser(app.db, { | |
| 34 | origin: "seed", | |
| 35 | trusted: true, | |
| 36 | username: "owner", | |
| 37 | allowReserved: true, | |
| 38 | email: "[email protected]", | |
| 39 | password: "a-good-passphrase", | |
| 40 | adminIfFirst: true, | |
| 41 | }); | |
| 42 | if (!made.ok) throw new Error(made.error); | |
| 43 | promote(app.db, made.value); | |
| 44 | const owner: User = made.value; | |
| 45 | runtime = new AppsRuntime(app, { | |
| 46 | domain: "front.test", | |
| 47 | scheme: "http", | |
| 48 | publicPort: 0, | |
| 49 | forgeUrl: "http://localhost", | |
| 50 | }); | |
| 51 | const status = spyOn(AppsRuntime.prototype, "status").mockImplementation( | |
| 52 | (): EnvStatus[] => [{ env: "production", state: "running", commitOid: HEAD } as EnvStatus], | |
| 53 | ); | |
| 54 | restore = () => status.mockRestore(); | |
| 55 | for (let n = 0; n < APPS; n++) { | |
| 56 | const name = `speed${n}`; | |
| 57 | const repo = await createRepo(app, owner, { name, visibility: "public", description: name }); | |
| 58 | if (!repo.ok) throw new Error(String(repo.error)); | |
| 59 | const record = insertApp(app.db, { repoId: repo.value.id, slug: name, access: "public" }); | |
| 60 | if (!record.ok) throw new Error(record.error); | |
| 61 | const id = (record.value as AppRecord).id; | |
| 62 | mkdirSync(join(dataDir, "looks", id), { recursive: true }); | |
| 63 | writeFileSync(join(dataDir, "looks", id, "thumb.png"), PNG); | |
| 64 | recordLook(app.db, { | |
| 65 | appId: id, | |
| 66 | env: "production", | |
| 67 | commitOid: HEAD, | |
| 68 | kind: "thumb", | |
| 69 | path: `looks/${id}/thumb.png`, | |
| 70 | }); | |
| 71 | repoIds.push(repo.value.id); | |
| 72 | } | |
| 73 | }, 60_000); | |
| 74 | ||
| 75 | afterAll(async () => { | |
| 76 | restore(); | |
| 77 | await runtime.stop(); | |
| 78 | app.close(); | |
| 79 | await rm(dataDir, { recursive: true, force: true }); | |
| 80 | }); | |
| 81 | ||
| 82 | test("a second request is the page already built, not a new one", () => { | |
| 83 | const first = frontPage(app); | |
| 84 | expect(first.total).toBe(APPS); | |
This passes with no cache at 30 apps. Assert identity of the returned page on the second call (toBe(first)), which fails without the cache. Remove the console.log and the timing threshold. |
||
| 85 | expect(first.shelf.length).toBeGreaterThan(0); | |
| 86 | expect(frontPage(app)).toBe(first); | |
| 87 | }); | |
| 88 | ||
| 89 | test("forgetting the front scores drops the kept page", () => { | |
| 90 | const first = frontPage(app); | |
| 91 | expect(frontPage(app)).toBe(first); | |
| 92 | forgetFrontScores(app); | |
| 93 | expect(frontPage(app)).not.toBe(first); | |
| 94 | }); | |
| 95 | ||
| 96 | test("making one app private builds a new page without it on the next request", () => { | |
| 97 | const before = frontPage(app); | |
outdated
This also passes without the cache. Pair it with: warm call returns the same object, then after the UPDATE a different object. That is the test that would fail if the key were wrong. |
||
| 98 | expect(frontPage(app)).toBe(before); | |
| 99 | const gone = before.shelf[0]!.repo; | |
| 100 | app.db.query("UPDATE repo SET visibility = 'private' WHERE id = ?1").run(gone.id); | |
| 101 | const after = frontPage(app); | |
| 102 | expect(after).not.toBe(before); | |
| 103 | expect(after.total).toBe(APPS - 1); | |
| 104 | expect([after.featured, ...after.shelf].some((e) => e?.repo.id === gone.id)).toBe(false); | |
| 105 | }); |
new above
offline
Low: on a hit the page returned was built with an earlier
now, so a caller passing its ownnow(as tests do) can get an answer that ignores it. Either includenowin the condition (now - kept.now < PAGE_MS, droppingperformance.now()) or note in the doc comment thatnowonly counts on a rebuild.