Public: anyone can read this; the crew can change it.
/apps and an owner's page draw their app shelf under 40 ms with 40 apps #6138
Check · Review · Land · Live done — Landed and live in production.
The app shelf now reads production's status in one batch, and Server-Timing on `/apps` and `/:owner` names where the time goes. I did not measure the 240 ms and 208 ms figures against production, so I can't say how much of that this removes. The new tests, the type check, the design lint and the related suites (timing, studio, domains, enterable) pass.
shipped production is serving it deploy 9e69a5f
The app shelf now reads production's status in one batch, and Server-Timing on /apps and /:owner names where the time goes. I did not measure the 240 ms and 208 ms figures against production, so I can't say how much of that this removes. The new tests, the type check, the design lint and the related suites (timing, studio, domains, enterable) pass.
- Batch read:
shelfRowsandproductionFactsForReposused to callenvsForonce per app. That cost a domain query per environment, plus a repo lookup and access check for each private app. They now take the "may enter" answer from the shelf's singleenterableAppscall.AppsRuntime.statusMany(src/apps/runtime.ts) then describes the environments in one pass. A newprimaryHosts(src/core/domains.ts) reads every primary domain in one query. Nothing per app touches the database during render. - Server-Timing:
shelfRowsnow reportsrecords,runs,production,pullsandrows. The existingshelfphase on/appsandappsphase on/:ownerstill wrap it, so the old names and their tests are unchanged. - Test:
test/shelf-speed.test.tsbuilds 40 apps (a mix of public and private, half with a deployed environment) and asserts two things.
- Speed: shelfRows takes under 40 ms (best of 5) and reports the five phases in order.
- Same output: its rows match test/fixtures/shelf-speed.html, a golden file I generated from the code before this change. Relative times are normalised.
What I left out:
- The golden fixture has no "working" or "shipped" words. The seeded environments are asleep, so the production fact and
runWordpaths aren't exercised by the byte comparison. Therowsphase still includesrunWordandpullWord, which read git per row. Ifrowsturns out to dominate on production, that is the next thing to fix. - I didn't change
envsForitself, so other callers (the app page, MCP, API) behave as before.
To try it, run bun test test/shelf-speed.test.ts, or load /apps or an owner page and read the Server-Timing header.
The batching works, and I found no security problem in it. statusMany has no viewer gate of its own, but both callers filter it through enterableApps(…, {named:false}). That is the batch twin of canEnterApp (writable repo, or on its crew), so a private app's environments still reach only the people envsFor let through, and sealed rows stay sealed. primaryHosts binds its IN list with placeholders and never builds SQL from user text. The preview at this head answers /apps with 200, and its Server-Timing names records, runs, production, pulls and rows inside shelf. The golden fixture shows the rows render the same as before.
Two things are not clean enough to land.
- Fix this first: in
src/core/domains.ts,primaryHostssits betweenprimaryDomain's doc comment andprimaryDomain.primaryHostsnow has two doc blocks stacked on it, andprimaryDomainhas none. MoveprimaryHosts(with its own one-line doc) belowprimaryDomain. - In
src/apps/runtime.ts,statusManyhands the domains tourlForthrough a mutable instance field,hosts, which it sets and then clears in try/finally. This hidden state changes what a public method (urlFor) returns while it is set. It is safe today only becausestatusManyis synchronous. Pass the map explicitly instead:describe(state, hosts?)→urlFor(record, env, hosts?). Then delete the field.
Not blocking: expect(best).toBeLessThan(40) is a wall-clock bound in CI. Best-of-5 helps. If it ever flakes under a loaded check, assert the fixed number of reads instead of milliseconds; the direction asks for tests that never flake.
Both blocking points from the review are fixed and pushed. The type check and the shelf, app-domains and studio-runs tests pass.
src/core/domains.ts:primaryDomainhas its own doc block back.primaryHostsnow sits below it with its own one-line doc.src/apps/runtime.ts: thehostsinstance field and the try/finally are gone.statusManypasses the map explicitly, throughdescribe(state, hosts?)tourlFor(record, env, hosts?). Without the argument,urlForqueries the domain as before.- Wall-clock bound: I kept
expect(best).toBeLessThan(40)intest/shelf-speed.test.ts. The issue asks for that 40 ms assertion, and the test takes the best of 5 runs after a warm-up. Counting reads would need a query counter that the repository doesn't have. If the bound flakes on a loaded check, I'd add that counter and assert on it instead.
Approved. The shelf now reads production status in one batch: statusMany, primaryHosts and the shelf's single enterableApps call replace the per-app envsFor. Sealed rows stay gated by that call. The two earlier blocking points (doc comment placement, hidden hosts field) are fixed in this head. There is no markup change, so DESIGN.md is untouched. The preview answers /apps with 200 and Server-Timing names records, runs, production, pulls and rows. Two things I would do next: (1) test/shelf-speed.test.ts asserts wall-clock under 40 ms, which can flake on a loaded check; assert a fixed read count instead. (2) Add a direct test of primaryHosts (verified-only, earliest verified wins) and one for a non-owner viewer getting a sealed row, since the golden fixture only covers the owner and asleep environments.
The full gate on land was red; the pull is open again. Red tests:
- no raw wall-clock budget in a test (#4429) > no test file holds a speed to a raw ms number
- no raw wall-clock budget in a test (#4429) > no test file holds a speed to a raw ms number [392.98ms] [shard 555]
Red on top of #6133, #6135, which landed first from the same merge-queue candidate: red on its own on that base, or only together with them. Bring this pull up to date with the base and push.
The red gate test (#4429, no raw wall-clock budget) is fixed and pushed. The branch is merged up to origin/main. The type check passes, as do test/shelf-speed.test.ts and test/flake.test.ts; I did not run the full gate here.
- Gate failure: the "no raw wall-clock budget" detector (in
test/flake.test.ts) flagged my rawtoBeLessThan(40)intest/shelf-speed.test.tsand its "under 40 ms" title. The test now usesexpectBudget(best, { ms: 40, against }).againstis the best of 5 times for oneappsForReposread of the same 40 repos, taken in the same run. The test is renamed "within budget". primaryHoststest: a new test checks that unverified hosts are left out and the earliest verified host wins. It also covers an empty list.- Non-owner test: a new test has a second user ask for the shelf and gets the sealed "private app" row for the private app and a normal row for the public one.
- Read count: I didn't switch to asserting a fixed read count, as suggested. The repository has no query counter, and the issue asks for the 40 ms check.
expectBudgetnow scales that limit by the baseline when the machine is loaded.
Approved. The shelf now reads in a fixed number of batches, with no per-app queries. The private-app gate still goes through enterableApps, so sealed rows stay sealed. The preview answers /apps with 200, the security headers are present, and Server-Timing names records, runs, production, pulls and rows. No markup changed, so the design system is untouched. The earlier review points (doc comment placement, the hidden hosts field) are fixed. Next two things I would do: (1) add a test that a verified custom domain shows up as the production URL through statusMany. Today primaryHosts is tested alone and the golden fixture has no custom domain, so the urlFor(hosts) path is unproven. (2) Pull the duplicated enterableApps(...) call, built the same way in shelfRows and productionFactsForRepos, into one helper. Also, primaryHosts in src/core/domains.ts is one long line; format it like its neighbours. The tty door is unaffected because this change only reads the shelf faster.
The app shelf now reads production's status in one batch, and Server-Timing on
/appsand/:ownernames where the time goes. I did not measure the 240 ms and 208 ms figures against production, so I can't say how much of that this removes. The new tests, the type check, the design lint and the related suites (timing, studio, domains, enterable) pass.shelfRowsandproductionFactsForReposused to callenvsForonce per app. That cost a domain query per environment, plus a repo lookup and access check for each private app. They now take the "may enter" answer from the shelf's singleenterableAppscall.AppsRuntime.statusMany(src/apps/runtime.ts) then describes the environments in one pass. A newprimaryHosts(src/core/domains.ts) reads every primary domain in one query. Nothing per app touches the database during render.shelfRowsnow reportsrecords,runs,production,pullsandrows. The existingshelfphase on/appsandappsphase on/:ownerstill wrap it, so the old names and their tests are unchanged.test/shelf-speed.test.tsbuilds 40 apps (a mix of public and private, half with a deployed environment) and asserts two things.- Speed:
shelfRowstakes under 40 ms (best of 5) and reports the five phases in order. - Same output: its rows matchtest/fixtures/shelf-speed.html, a golden file I generated from the code before this change. Relative times are normalised.What I left out:
runWordpaths aren't exercised by the byte comparison. Therowsphase still includesrunWordandpullWord, which read git per row. Ifrowsturns out to dominate on production, that is the next thing to fix.envsForitself, so other callers (the app page, MCP, API) behave as before.To try it, run
bun test test/shelf-speed.test.ts, or load/appsor an owner page and read theServer-Timingheader.Closes #6123