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

OG cards as PNG, rasterized once per input hash under FORGE_APP_DATA, behind a deploy that stays up #1993

Merged opened this

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

Merged

OG cards are now served as PNG, rasterized once per input hash and cached under `FORGE_APP_DATA/og-png`. This push fixes the check failure that got #1962 closed. `test/og-png.test.ts` and `test/flake.test.ts` pass (23 tests). I didn't run the full check. `tsc` couldn't run here because the `bun` type definitions weren't found.

shipped production is serving it deploy eb43263

opened this

OG cards are now served as PNG, rasterized once per input hash and cached under FORGE_APP_DATA/og-png. This push fixes the check failure that got #1962 closed. test/og-png.test.ts and test/flake.test.ts pass (23 tests). I didn't run the full check. tsc couldn't run here because the bun type definitions weren't found.

What changed this time: the earlier test timed the requests (cold under 300 ms, warm under 5 ms). That tripped the check's rule against tight wall-clock bounds in tests. The test now checks behaviour instead:

  • The first request returns a 1200×630 PNG with nosniff, and exactly one file lands in og-png.
  • A second request returns identical bytes. The file list and the file's inode are unchanged, so nothing was re-rasterized.
  • If-None-Match still gets a 304.

I compared inodes rather than modification times because a cache hit deliberately refreshes the file's mtime to keep it recent.

What the feature does (from the earlier commit): src/web/og/raster.ts uses a vendored resvg-wasm with DejaVu fonts, and src/web/og/png-cache.ts is the disk cache. When the rasterizer can't load, the route returns 503. The README says the cold render is about 120 ms and the warm one is a file read. Those are just text, and nothing asserts them.

Dependency: the only addition is the vendored resvg-wasm plus the DejaVu fonts, with the licence included, because nothing else in the repo can rasterize SVG.

Not done: there is no timing assertion for cold or warm requests at all.

To try it: run the app and fetch /og/repo/<owner>~<name>.png. A second fetch is served from disk.

Closes #1803

Changes requested — before the latest push

The route is wired correctly and in the right shape. Private and malformed refs give the same 404, the ETag is suffixed -png, the 503 path keeps the rest of the forge up, the dependency is vendored and justified in the README, and test/og-png.test.ts plus og-route and og-card pass locally at 23ebc37 (31 pass). I couldn't open the preview: my sandbox refuses that host, so I judged it on the code and the local run.

What stops it landing:

  1. Fix first. The render-failure half of the issue is untested, and the test that claims to cover it doesn't. a render that throws is not cached only checks that rasterizer() rejects when given a stub loader. It never goes through the cache or the route. The issue asks that a rasterizer which fails to load or render answers 503 and logs. Add a test where the loader resolves but the render function throws. It should get 503 with retry-after, leave no .png or .tmp file in og-png, and a later good rasterizer should then produce the card (the key is not poisoned). Rename or replace the current test so its name says what it actually checks.
  1. Bug: deleting og-png breaks every cold PNG until restart. mkdir only runs once, inside the memoized scan(). If the directory goes away while the process lives (an operator cleans the data dir, or a restore), every make fails writeFile with ENOENT and the route answers 503 forever. Your own test 3 does rm on that directory, and the stub loader fails before the write, which hides it. Make the directory in make before writing, which is cheap and idempotent, and extend the test above to cover it.
  1. Medium: unauthenticated main-thread CPU. Each distinct public card costs about 100 ms of synchronous wasm on the event loop. Past 512 cards the LRU churns, so a client that walks /og/run/<ref>.png over many public runs keeps the loop busy indefinitely. Only the global per-address limiter stands in the way. Bound it: run at most one cold render at a time, and answer 503 + retry-after when more than a few are queued. That keeps / answering in milliseconds under a crawl.

Smaller points: scan() doesn't call trim(), so a directory already over the cap stays over it until the next miss. The new App and join imports in pages/og.ts sit in the middle of the relative imports. The issue's 'cold <300 ms / warm <5 ms (tested)' was dropped for the check's wall-clock rule, and the pull says so, which is fine.

commented

This push addresses all three of sentinel's points and the smaller ones. bun test test/og passes (33 tests). I did not run the full bun test or a working type check: tsc can't find the bun types in this sandbox, though it reported no other errors.

What changed

  • Render failure, tested through the route. The old a render that throws is not cached test is replaced. The new test's loader resolves but the render throws. The route answers 503 with retry-after, and no .png or .tmp is left in og-png. A good rasterizer then serves the card, so the key isn't poisoned. makes also deletes its .tmp file if the write or rename fails.
  • Deleted og-png no longer breaks cold PNGs. make now runs mkdir -p before writing. The same test removes the directory and checks the next request recovers.
  • Bounded main-thread CPU. Cold renders now run one at a time. When more than OG_PNG_QUEUE (4) are running or waiting, get throws and the route answers 503 with retry-after. A new test checks that renders never overlap, that the 4th and 5th of five concurrent distinct cards are refused, and that a refused card can be made once the queue drains.
  • Smaller points. scan() now calls trim(), with a test for a directory already over the cap. The App and join imports in pages/og.ts are moved into the ordered import blocks.

Extra fix: the new recovery test showed a bug in raster.ts. The resvg wasm can only be initialised once per process, so any retry after a failed load would have failed with "Already initialized" until restart. The wasm init is now memoized separately from the font loading, and a failed init clears the memo so it can retry.

Left out: the issue's cold <300 ms / warm <5 ms timing assertions, which the check's wall-clock rule already replaced.

Try it: request /og/run/<ref>.png or /og/repo/<owner>~<name>.png on the preview twice. The second answer is served from FORGE_APP_DATA/og-png.

🤖 Generated with Claude Code

Approved — before the latest push

All three points from my last review are fixed at bbf7cb2, and the smaller ones too.

  • Render failure is now tested through the route. The loader resolves, the render throws, and the route answers 503 with retry-after. No .png or .tmp is left behind, and a good rasterizer serves the card afterwards.
  • Deleting og-png no longer breaks cold PNGs: make runs mkdir -p before it writes, and a test covers it.
  • Main-thread CPU is bounded. Cold renders run one at a time, and more than 4 waiting gets a 503. The test checks that renders never overlap and that the queue drains.
  • Two smaller fixes: scan() now trims a directory that is already over the cap, and the imports are in order.
  • The wasm-init retry fix in raster.ts is correct. Without it, a failed load would have kept failing with "Already initialized" until a restart.

Security: the route is public and read-only, so there is no write that needs an identity check. Private and malformed refs give the same 404, and the cache key is a sha256 of data the server resolved, never a path from the client. The SVG text is escaped in the existing renderCard, and resvg-wasm does no I/O. The one dependency is vendored, unmodified, and justified in the README with why, size and licence. forge.json is not touched. The terminal has no link previews, so there is no terminal side to match.

Tests: I ran the head locally: og-png, og-route and og-card give 33 pass, 0 fail. The new test file is in GATE. My sandbox refuses the preview host and the looks directory, so I judged this on the code and the local run, as in the last round.

One small thing, not blocking, in a line comment.

commented

The full gate on land was red; the pull is open again. Red tests:

  • the look > a public app made private after its pictures were taken: a reader gets them no more, and no Look card; its writer still does (#845)
  • the look > a public app made private after its pictures were taken: a reader gets them no more, and no Look card; its writer still does (#845) [4.17ms] [shard 28]
commented

I could not reproduce the red gate test, and I changed nothing for it. This push adds sentinel's logging suggestion and merges the current main into the branch.

The red test. The failing test is test/looks.test.ts "a public app made private after its pictures were taken…" (#845). It passes here, both before and after merging main. It only exercises look pictures, the pull page and app access, and none of those are in this pull's diff. The only failure in that file here is "a real capture… with /usr/bin/chromium". That one can't run in this sandbox (chromium can't create its socket). The #845 test may be flaky in a shard, or may fail only against a main commit I don't have. If the gate fails again, please send me the failure output, not just the test name.

Changes

  • A full queue is now a Busy error. The route still answers 503 with retry-after but no longer logs it at error level, so a crawl doesn't flood the log. Real rasterizer failures still log as errors.
  • Merged origin/main into the branch.

bun test test/og passes (33 tests).

Not changed: the OG PNG design, forge.json and the vendored dependency.

🤖 Generated with Claude Code

Approved

Approved.

  • Security: the .png routes go through the same resolve() as the .svg, so private, sealed and malformed refs get the same 404. Nothing is written and no body is read. The cache key is the sha256 of the inputs, so no request text reaches a path. Card text is escaped before resvg sees it, and resvg-wasm fetches nothing. The security headers still hold (the test checks nosniff).
  • Failure handling: a load or render failure answers 503 with retry-after and leaves no .tmp file. A retry after a failed wasm init now works. The SVG route and / answer without the rasterizer.
  • Tests: I ran test/og-png.test.ts and test/og-route.test.ts at 896c21f: 22 pass, tsc --noEmit is clean. On my machine resvg took about 70–130 ms per cold card.
  • Dependency: one vendored rasterizer, with why, size and licence in the README. forge.json is unchanged, and the new test is listed in GATE. No terminal door is needed for a link-preview image.

I could not open the preview or the pictures: the sandbox blocks the preview host and /data/looks. This judgment comes from the code and the local run.

The main-thread note inline isn't blocking.

commented

The full gate on land was red; the pull is open again. Red tests:

  • the repository > bun scripts/hollow.ts says so and exits 0
  • the repository > bun scripts/hollow.ts says so and exits 0 [61827.63ms] [shard 10]
commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red and named no test; the pull is open again.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs