Public: anyone can read this; the crew can change it.

forked from trevato/cs

The numbers a page boasts are measured, not typed: the bundle sizes on /example are read from the files they describe #207

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/cs-engine:torque/128-the-numbers-a-page-boasts-are-measured-n → main 1 commit fork

  • Merged. torque/128-the-numbers-a-page-boasts-are-measured-n was merged into main as 44ea77f.

Review

Sign in to review this change.
10 files changed +374 −25
README.md +16
@@ -797,6 +797,22 @@ never checked itself.
797797 SVG data URI of the crosshair — so **`/favicon.ico` is a request no browser
798798 makes here**: no file under `static/`, no second round trip, and the CSP
799799 already allows `img-src 'self' data:`.
800 - `src/cost.ts` and `src/bytes.ts` — **the numbers `/example` states about
801 itself are measured, not typed.** That page's whole claim is what a third game
802 costs, so `src/cost.ts` weighs the committed bundles in `static/` with
803 `scripts/build.ts`'s own `weigh` (bytes on disk, and gzip at level 9, the
804 pinned ruler the ceilings are held to) and counts the lines of
805 `src/engine/example.ts` and of `src/example/`. It runs **once, at start-up**,
806 in `src/server.ts`, which hands the result to the page: the page renders, and
807 a page does not read files on the request path. `src/bytes.ts` is the
808 formatter and nothing else — `9,735` when the exact count is the claim,
809 `4.5 kB` when the feel of the download is (a kB here is 1,024 bytes) — so the
810 rounding and the grouping have a test rather than living inside a template.
811 `src/pages/example.test.ts` reads the bundles and the sources itself, formats
812 them the same way and looks for them in the rendered page, and scans every
813 digit in those paragraphs for one nobody measured, so **a bundle that grows
814 moves the sentence or fails the run**. The page says so in its own last
815 paragraph, next to the numbers.
800816 - `src/games.ts` is **the registry: a game is one entry there.** It holds the
801817 name its bundle is filed under, the path it answers on, what the header calls
802818 it, the source it is bundled from, the size that bundle may not pass and the
scripts/build.test.ts +16
@@ -11,6 +11,7 @@ import {
1111 overshoot,
1212 sizes,
1313 STATIC_DIR,
14 weigh,
1415 write,
1516 type Entry,
1617 } from "./build.ts";
@@ -156,6 +157,21 @@ test("the committed bundle of every entry is under its ceiling", () => {
156157 }
157158 });
158159
160 test("weighing a committed bundle reads the file and builds nothing", async () => {
161 const { dir, entries } = fixture();
162 const [built] = await write(entries, dir);
163 const file = readFileSync(join(dir, built?.file as string));
164 // The same ruler `sizes` is: on disk, and gzipped at the pinned level.
165 expect(weigh("app", dir)).toEqual(sizes(file));
166 expect(weigh("app", dir).raw).toBe(file.byteLength);
167 // And it is the artifact, not a fresh build: a file edited in place weighs
168 // what it now is, which is the point of quoting it on a page.
169 writeFileSync(join(dir, built?.file as string), `${built?.code as string}// and more\n`);
170 expect(weigh("app", dir).raw).toBeGreaterThan(file.byteLength);
171 // A game with nothing in static/ is the sentence `bundleOf` writes, not a zero.
172 expect(() => weigh("nowhere", dir)).toThrow(/has no bundle/);
173 });
174
159175 test("the build prints each bundle's raw and gzipped size next to its name", () => {
160176 const code = `console.log("hi");\n`;
161177 const { raw, gzip } = sizes(code);
scripts/build.ts +15 −1
@@ -7,7 +7,7 @@
77 * weighs both the committed file and a fresh build against it.
88 */
99
10 import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
10 import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1111 import { join, resolve } from "node:path";
1212 import { type Ceiling, GAMES } from "../src/games.ts";
1313
@@ -96,6 +96,20 @@ export function bundleOf(name: string, files: readonly string[], where = STATIC_
9696 );
9797 }
9898
99 /**
100 * What the committed bundle of `name` weighs right now: the one
101 * `<name>.<hash>.js` in `dir`, on disk and gzipped at `GZIP_LEVEL`. It builds
102 * nothing — this is the file a browser downloads, weighed by the same ruler the
103 * ceilings are held to, so a page that quotes a size and a test that checks a
104 * ceiling are reading one number.
105 *
106 * A missing or duplicated bundle throws here, through `bundleOf`, with the
107 * sentence that says what to run about it.
108 */
109 export function weigh(name: string, dir = STATIC_DIR): Ceiling {
110 return sizes(readFileSync(join(dir, bundleOf(name, readdirSync(dir), dir))));
111 }
112
99113 export type Built = { name: string; file: string; code: string; ceiling: Ceiling };
100114
101115 /** Bundle every entry and give each the file name its own contents deserve. */
src/bytes.test.ts added +31
@@ -0,0 +1,31 @@
1 import { expect, test } from "bun:test";
2 import { bytes, kB } from "./bytes.ts";
3
4 test("an exact count is grouped in threes, from either side of a thousand", () => {
5 expect(bytes(0)).toBe("0");
6 expect(bytes(7)).toBe("7");
7 expect(bytes(999)).toBe("999");
8 expect(bytes(1000)).toBe("1,000");
9 expect(bytes(9735)).toBe("9,735");
10 expect(bytes(93_714)).toBe("93,714");
11 expect(bytes(1_234_567)).toBe("1,234,567");
12 });
13
14 test("a kilobyte is 1,024 bytes, to a tenth, with the unit on it", () => {
15 // The number the issue this came from wrote by hand: 4,229 over the wire is
16 // 4.1 kB. It is 1,024 and not 1,000, and that is the whole difference.
17 expect(kB(4229)).toBe("4.1 kB");
18 expect(kB(4619)).toBe("4.5 kB");
19 expect(kB(1024)).toBe("1.0 kB");
20 expect(kB(512)).toBe("0.5 kB");
21 expect(kB(0)).toBe("0.0 kB");
22 expect(kB(42_539)).toBe("41.5 kB");
23 });
24
25 test("a number that is not a count of bytes is a throw, not a sentence with NaN in it", () => {
26 expect(() => bytes(-1)).toThrow(/whole number of bytes/);
27 expect(() => bytes(1.5)).toThrow(/whole number of bytes/);
28 expect(() => bytes(Number.NaN)).toThrow(/whole number of bytes/);
29 expect(() => kB(-1)).toThrow(/whole number of bytes/);
30 expect(() => kB(Number.POSITIVE_INFINITY)).toThrow(/whole number of bytes/);
31 });
src/bytes.ts added +27
@@ -0,0 +1,27 @@
1 /**
2 * A byte count, written the way a sentence says one. Two shapes and nothing
3 * else: `9735` is `9,735` when the exact figure is the claim, and `4619` is
4 * `4.5 kB` when what a reader wants is the feel of the download.
5 *
6 * It is a module of its own because the alternative is arithmetic inside a
7 * template, where no test can reach it: a page interpolates `bytes(n)` and the
8 * rounding, the grouping and the unit are decided here, once, with a test
9 * beside them.
10 *
11 * The unit is stated rather than assumed: a kB here is 1,024 bytes, which is
12 * what makes 4,619 read as 4.5 kB. That is the ruler `scripts/build.ts`
13 * measures with too — a gzip figure at level 9 — and the pages that print these
14 * numbers say so next to them.
15 */
16
17 /** A count this module will print: a whole number of bytes, and not a negative one. */
18 const whole = (n: number, kind: string): number => {
19 if (!Number.isInteger(n) || n < 0) throw new Error(`${kind} wants a whole number of bytes, not ${n}`);
20 return n;
21 };
22
23 /** `9735` → `9,735`: the exact count, in groups of three. */
24 export const bytes = (n: number): string => String(whole(n, "bytes")).replace(/\B(?=(\d{3})+$)/g, ",");
25
26 /** `4619` → `4.5 kB`: the same count rounded to a tenth of a kibibyte, with its unit. */
27 export const kB = (n: number): string => `${(whole(n, "kB") / 1024).toFixed(1)} kB`;
src/cost.ts added +77
@@ -0,0 +1,77 @@
1 /**
2 * What the example game cost, measured from the files that cost it.
3 *
4 * `/example` exists to say what a third game on this engine is worth in bytes
5 * and in lines. A number typed into that prose is wrong the first time a bundle
6 * grows — the shooter's went from 71,646 to 93,714 while the sentence sat
7 * still — so nothing here is typed: the bundle sizes are the committed files in
8 * static/ weighed by `scripts/build.ts`'s own ruler, and the line counts are
9 * the sources read and counted.
10 *
11 * It reads files, so it is called once, at start-up, by `src/server.ts`, and
12 * the result is handed to the page. A page that measured itself per request
13 * would do file I/O on the request path for a number that cannot change while
14 * this process lives.
15 *
16 * It describes what *this* process serves: the bundle in this deployment's
17 * static/ and the sources in this deployment's tree, which is the honest thing
18 * for a page to claim about its own download.
19 */
20
21 import { readFileSync } from "node:fs";
22 import { join } from "node:path";
23 import { weigh } from "../scripts/build.ts";
24 import type { Ceiling } from "./games.ts";
25
26 /**
27 * The level every gzip figure in this app is measured at, re-exported so a page
28 * can name it in its prose without importing the build script: the number the
29 * sentence quotes and the number the bytes were weighed at are one constant.
30 */
31 export { GZIP_LEVEL } from "../scripts/build.ts";
32
33 const ROOT = join(import.meta.dir, "..");
34
35 /** The game itself: the lines the engine's guide is written out of. */
36 export const GAME_SOURCE = "src/engine/example.ts";
37
38 /** The wiring a third game needed around it, and the whole of it. */
39 export const WIRING_SOURCES = ["src/example/main.ts", "src/example/plan.ts"];
40
41 /** Every number `/example` states about itself. Bytes are bytes; lines are lines. */
42 export type Cost = {
43 /** The example's committed bundle: what the page just downloaded. */
44 script: Ceiling;
45 /** The shooter's committed bundle, which the page compares itself against. */
46 shooter: Ceiling;
47 /** Lines of `GAME_SOURCE`. */
48 game: number;
49 /** Lines of `WIRING_SOURCES`, added up. */
50 wiring: number;
51 };
52
53 /**
54 * How many lines a source is, counted the way an editor numbers them: a
55 * trailing newline ends the last line rather than starting an empty one, so a
56 * file `wc -l` calls 79 lines is 79 here.
57 */
58 export function lineCount(text: string): number {
59 const lines = text.split("\n");
60 if (lines.at(-1) === "") lines.pop();
61 return lines.length;
62 }
63
64 const linesOf = (root: string, file: string): number => lineCount(readFileSync(join(root, file), "utf8"));
65
66 /**
67 * Weigh and count, now. `root` and `dir` are arguments so a test can point this
68 * at a tree of its own; the server calls it with neither.
69 */
70 export function measureExample(root = ROOT, dir?: string): Cost {
71 return {
72 script: weigh("example", dir),
73 shooter: weigh("app", dir),
74 game: linesOf(root, GAME_SOURCE),
75 wiring: WIRING_SOURCES.reduce((total, file) => total + linesOf(root, file), 0),
76 };
77 }
src/pages/example.test.ts added +114
@@ -0,0 +1,114 @@
1 /**
2 * The numbers `/example` boasts, against the files they describe.
3 *
4 * The page's whole claim is that a third game on this engine is cheap, and the
5 * evidence is four numbers: the bytes of its bundle, the bytes over the wire,
6 * the bytes of the shooter's bundle, and the lines the game and its wiring are.
7 * Typed into the prose they drift — the shooter's sat at 71,646 while the file
8 * grew past 93,000 — so this file reads the bundles and the sources itself,
9 * formats them the way the page does, and looks for them in the rendered
10 * document. A bundle that grows now moves the sentence or fails the run.
11 *
12 * It measures independently of `src/cost.ts` where it can: the sizes here are
13 * `readFileSync` plus `Bun.gzipSync` at the pinned level, so a mistake in the
14 * measuring module is a mismatch here rather than the same wrong number twice.
15 */
16
17 import { expect, test } from "bun:test";
18 import { readdirSync, readFileSync } from "node:fs";
19 import { join } from "node:path";
20 import { bundleOf, GZIP_LEVEL, STATIC_DIR } from "../../scripts/build.ts";
21 import { bytes, kB } from "../bytes.ts";
22 import { GAME_SOURCE, lineCount, measureExample, WIRING_SOURCES } from "../cost.ts";
23 import { costHtml, examplePage } from "./example.ts";
24
25 const ROOT = join(import.meta.dir, "..", "..");
26
27 /** The committed bundle of a game, weighed here rather than asked for. */
28 const weighed = (name: string): { raw: number; gzip: number } => {
29 const file = readFileSync(join(STATIC_DIR, bundleOf(name, readdirSync(STATIC_DIR))));
30 return { raw: file.byteLength, gzip: Bun.gzipSync(file, { level: GZIP_LEVEL }).byteLength };
31 };
32
33 /** The same count `lineCount` makes, arrived at the other way round. */
34 const linesOf = (file: string): number => {
35 const text = readFileSync(join(ROOT, file), "utf8");
36 return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
37 };
38
39 /**
40 * The page as one line: prose is wrapped in the template, so a sentence is read
41 * here with its newlines and indentation collapsed to single spaces. What is
42 * being checked is the words and the numbers, never where the source wrapped.
43 */
44 const rendered = (): string =>
45 examplePage({
46 env: "local",
47 pull: undefined,
48 commit: "abc1234",
49 script: "/static/example.x.js",
50 ssh: undefined,
51 cost: measureExample(),
52 }).replace(/\s+/g, " ");
53
54 test("a line count is what an editor would number, trailing newline and all", () => {
55 expect(lineCount("a\nb\nc\n")).toBe(3);
56 expect(lineCount("a\nb\nc")).toBe(3);
57 expect(lineCount("only one")).toBe(1);
58 expect(lineCount("")).toBe(0);
59 expect(lineCount("\n")).toBe(1);
60 // The real file, counted the way `wc -l` counts it.
61 const source = readFileSync(join(ROOT, GAME_SOURCE), "utf8");
62 expect(lineCount(source)).toBe(source.split("\n").length - 1);
63 });
64
65 test("what the page is handed is what the files weigh and what the sources count", () => {
66 const cost = measureExample();
67 expect(cost.script).toEqual(weighed("example"));
68 expect(cost.shooter).toEqual(weighed("app"));
69 expect(cost.game).toBe(linesOf(GAME_SOURCE));
70 expect(cost.wiring).toBe(WIRING_SOURCES.reduce((total, file) => total + linesOf(file), 0));
71 // Sanity, so a measurement of zero can never quietly pass the checks above.
72 expect(cost.script.raw).toBeGreaterThan(1000);
73 expect(cost.shooter.raw).toBeGreaterThan(cost.script.raw);
74 expect(cost.game).toBeGreaterThan(20);
75 expect(cost.wiring).toBeGreaterThan(cost.game);
76 });
77
78 test("the bytes the page states are the bytes of the committed bundles", () => {
79 const body = rendered();
80 const script = weighed("example");
81 const shooter = weighed("app");
82 expect(body).toContain(`is ${bytes(script.raw)} bytes`);
83 expect(body).toContain(`${kB(script.gzip)} of them over the wire`);
84 expect(body).toContain(`against the shooter's ${bytes(shooter.raw)}`);
85 expect(body).toContain(`and ${kB(shooter.gzip)}.`);
86 expect(body).toContain(`gzipped at level ${GZIP_LEVEL}`);
87 });
88
89 test("the lines the page states are the lines of the sources it names", () => {
90 const body = rendered();
91 expect(body).toContain(`the ${linesOf(GAME_SOURCE)} lines the engine's guide is written out of`);
92 const wiring = WIRING_SOURCES.reduce((total, file) => total + linesOf(file), 0);
93 expect(body).toContain(`two files and ${wiring} lines`);
94 });
95
96 test("no number the page states about itself is one somebody typed", () => {
97 const body = rendered();
98 // The four that were typed, and were wrong by the time this was written.
99 for (const stale of ["8,754", "4,229", "71,646", "seventy-nine", "about two hundred"])
100 expect(body).not.toContain(stale);
101 // Every group of digits in the paragraphs about the cost is one of the
102 // measured numbers, the gzip level, or the 1,024 the unit is defined by. A
103 // number typed into this prose fails here.
104 const cost = measureExample();
105 const section = costHtml(cost);
106 const allowed = new Set([
107 bytes(cost.script.raw), bytes(cost.shooter.raw),
108 kB(cost.script.gzip).replace(" kB", ""), kB(cost.shooter.gzip).replace(" kB", ""),
109 String(cost.game), String(cost.wiring), String(GZIP_LEVEL), "1,024",
110 "2", // "2D on one canvas", and the two files of wiring
111 "404", // the page nobody had to edit
112 ]);
113 for (const found of section.match(/\d[\d,]*(?:\.\d+)?/g) ?? []) expect([...allowed]).toContain(found);
114 });
src/pages/example.ts +49 −22
@@ -10,11 +10,17 @@
1010 * thing to demonstrate is the price tag, not a third shooter.
1111 */
1212
13 import { bytes, kB } from "../bytes.ts";
14 import { type Cost, GZIP_LEVEL } from "../cost.ts";
1315 import { navFor, othersHtml } from "./games.ts";
1416 import { page, type PageOptions as ShellOptions, stage } from "./shell.ts";
1517
16 /** What the shell needs that this page cannot know: the environment it runs in. */
17 export type PageOptions = Pick<ShellOptions, "env" | "pull" | "commit" | "script" | "ssh">;
18 /**
19 * What the shell needs that this page cannot know: the environment it runs in,
20 * and what this game cost. The cost is measured by `src/cost.ts` at start-up
21 * and handed in, because a page renders and does not read files.
22 */
23 export type PageOptions = Pick<ShellOptions, "env" | "pull" | "commit" | "script" | "ssh"> & { cost: Cost };
1824
1925 const NOTE = `
2026 <h2>The smallest game this engine can run</h2>
@@ -25,6 +31,46 @@ const NOTE = `
2531 <div class="row"><button type="button" class="primary" data-action="start">Start</button></div>
2632 `;
2733
34 /**
35 * The two paragraphs about what a third game cost, with every number in them
36 * measured: the bundles weighed on disk, the sources counted. Nothing here
37 * formats a number itself — `../bytes.ts` does — and nothing here reads a file;
38 * it is handed a `Cost` and writes the sentence around it.
39 *
40 * Exported so `./example.test.ts` can read every digit in these paragraphs and
41 * hold each one against a measurement, without the games' own blurbs below
42 * being part of the question.
43 */
44 export function costHtml(cost: Cost): string {
45 const { script, shooter, game, wiring } = cost;
46 return ` <p>The game on this page is not on this page. It is <code>src/engine/example.ts</code>,
47 the ${game} lines the engine's guide is written out of — a room, a plate, a seeded spawn, a
48 fixed step and one shot per press — and it was already there, already tested, before this
49 route existed. What a third game cost was the wiring around it:
50 <code>src/example/main.ts</code> finds the canvas, hands the engine's controls the game's own
51 bindings table, runs the engine's fixed loop and calls the game's <code>step</code>;
52 <code>src/example/plan.ts</code> draws the result. That is two files and ${wiring} lines, one
53 registry entry in <code>src/games.ts</code>, this page, and one line in the map from a game's
54 name to its page. No branch was added to the server, and nothing in the header,
55 the 404 or the other two pages was edited: they read the registry, so this game appeared in
56 all of them the moment it was written down.</p>
57 <p>What it did not have to write is the longer list: no loop, no fixed step, no bindings table
58 reader, no touch buttons, no pointer handling, no ray against a box, no seeded random, no
59 vector maths, no panel, no bar, no clock, no text scaling — all of that is the package, and
60 this game reaches for it through one door, <code>src/engine/index.ts</code>. It is 2D on one
61 canvas: no WebGL, no atlas, no mesh, no sound and no database, and the door costs what you
62 use — the script this page just downloaded is ${bytes(script.raw)} bytes, ${kB(script.gzip)}
63 of them over the wire, against the shooter's ${bytes(shooter.raw)} bytes and ${kB(shooter.gzip)}.
64 Every rectangle, the crosshair, the countdown bar and the <code>m:ss</code> clock come out of
65 the engine's <code>overlay</code>, and nothing else draws here.</p>
66 <p>Every number in those two paragraphs is measured, not typed. The byte counts are this
67 deployment's own committed bundles under <code>static/</code>, read when this server started
68 and gzipped at level ${GZIP_LEVEL} — the ruler <code>scripts/build.ts</code> weighs the
69 ceilings with, and a kB here is 1,024 bytes. The line counts are those source files read the
70 same way. A bundle that grows moves this sentence on the next deploy, and
71 <code>src/pages/example.test.ts</code> fails if it does not.</p>`;
72 }
73
2874 export function examplePage(options: PageOptions): string {
2975 return page({
3076 ...options,
@@ -50,26 +96,7 @@ export function examplePage(options: PageOptions): string {
5096 }),
5197 sectionsHtml: ` <section>
5298 <h2>What this cost</h2>
53 <p>The game on this page is not on this page. It is <code>src/engine/example.ts</code>, the
54 seventy-nine lines the engine's guide is written out of — a room, a plate, a seeded spawn, a
55 fixed step and one shot per press — and it was already there, already tested, before this
56 route existed. What a third game cost was the wiring around it:
57 <code>src/example/main.ts</code> finds the canvas, hands the engine's controls the game's own
58 bindings table, runs the engine's fixed loop and calls the game's <code>step</code>;
59 <code>src/example/plan.ts</code> draws the result. That is two files and about two hundred
60 lines, one registry entry in <code>src/games.ts</code>, this page, and one line in the map
61 from a game's name to its page. No branch was added to the server, and nothing in the header,
62 the 404 or the other two pages was edited: they read the registry, so this game appeared in
63 all of them the moment it was written down.</p>
64 <p>What it did not have to write is the longer list: no loop, no fixed step, no bindings table
65 reader, no touch buttons, no pointer handling, no ray against a box, no seeded random, no
66 vector maths, no panel, no bar, no clock, no text scaling — all of that is the package, and
67 this game reaches for it through one door, <code>src/engine/index.ts</code>. It is 2D on one
68 canvas: no WebGL, no atlas, no mesh, no sound and no database, and the door costs what you
69 use — the script this page just downloaded is 8,754 bytes, 4,229 of them over the wire,
70 against the shooter's 71,646. Every rectangle, the crosshair, the countdown bar and the
71 <code>m:ss</code> clock come out of the engine's <code>overlay</code>, and nothing else draws
72 here.</p>
99 ${costHtml(options.cost)}
73100 <p>Nothing on this page is stored: no endpoint, no table and no cookie. The leaderboard, the
74101 database and the terminal belong to the games next door: ${othersHtml("/example")}</p>
75102 </section>`,
src/server.test.ts +15
@@ -3,6 +3,8 @@ import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
33 import { tmpdir } from "node:os";
44 import { join } from "node:path";
55 import { bundle, STATIC_DIR } from "../scripts/build.ts";
6 import { bytes, kB } from "./bytes.ts";
7 import { measureExample } from "./cost.ts";
68 import { GAMES } from "./games.ts";
79 import { matchOf, playKey } from "./matchid.ts";
810 import { NOTHING_POSTED } from "./client/pending.ts";
@@ -208,6 +210,19 @@ test("serves the example on its own route, with its own page and its own bundle"
208210 expect(response.headers.get("set-cookie")).toBeNull();
209211 });
210212
213 test("the example page's byte counts are the bundles this server is serving", async () => {
214 // The page's claim is about what a browser just downloaded, so it is checked
215 // over the wire: the numbers in the served document are the files in static/,
216 // weighed here. `src/pages/example.test.ts` holds the rest of that sentence —
217 // this is the wiring, that the server measures at start-up and hands it over.
218 const body = (await (await fetch(base + "/example")).text()).replace(/\s+/g, " ");
219 const cost = measureExample();
220 expect(body).toContain(`is ${bytes(cost.script.raw)} bytes`);
221 expect(body).toContain(`${kB(cost.script.gzip)} of them over the wire`);
222 expect(body).toContain(`the shooter's ${bytes(cost.shooter.raw)} bytes`);
223 expect(body).toContain(`the ${cost.game} lines`);
224 });
225
211226 test("the example page carries every id and action its entry reaches for", async () => {
212227 // src/example/main.ts finds these by getElementById and hands [data-action]
213228 // to the engine's controls; nothing type-checks that, so the page is checked
src/server.ts +14 −2
@@ -33,6 +33,7 @@ import { existsSync, readdirSync } from "node:fs";
3333 import { join } from "node:path";
3434 import { bundleOf, STATIC_DIR } from "../scripts/build.ts";
3535 import { bind } from "./bind.ts";
36 import { measureExample } from "./cost.ts";
3637 import { openDatabase, type Player, saveMatch } from "./db.ts";
3738 import { GAMES } from "./games.ts";
3839 import { createLimiter } from "./limit.ts";
@@ -229,6 +230,14 @@ export function findScripts(dir = STATIC_DIR, games = GAMES): Map<string, string
229230 }
230231 const scripts = findScripts();
231232
233 /**
234 * What `/example` says a third game cost, weighed and counted once, here,
235 * against the files this process serves: the committed bundles in static/ and
236 * the sources in this tree. `src/cost.ts` does the reading; the page formats
237 * what it is handed. Per request this is a lookup, not a file.
238 */
239 const exampleCost = measureExample();
240
232241 /**
233242 * How one game's page is drawn for one request: its document, and any header
234243 * of its own — the shooter's session cookie is the only one. The status, the
@@ -291,8 +300,11 @@ const RENDER: Record<string, Render> = {
291300 // nothing, so it has nothing to name a browser for. No CSS of its own either:
292301 // its style block is the shell's, exactly.
293302 range: { draw: (_request, script) => ({ body: rangePage({ env, pull, commit, script, ssh }) }) },
294 // The example: the guide's game with a page around it, and no session either.
295 example: { draw: (_request, script) => ({ body: examplePage({ env, pull, commit, script, ssh }) }) },
303 // The example: the guide's game with a page around it, no session either, and
304 // the measured cost of being a third game, weighed once above.
305 example: {
306 draw: (_request, script) => ({ body: examplePage({ env, pull, commit, script, ssh, cost: exampleCost }) }),
307 },
296308 };
297309
298310 // The read routes, resolved once at start-up: the path each game answers on,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs