Repository navigation
Commit 69fa23d
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DEVkZEEx5UmhSJgP3kcEfLX)_
Part of #25785 (the first step the triage comment 6397341079 names: the
validator and cache directive on the bundle). #25785 remains open for
its second half, keeping `metadataForms` out of the boot bundle, which
is a wire-shape change for `domain:spec`.
Clause-②: no
Metadata-protocol: none
The claim is 6407675488 (seat `domain:server#2`, card thread
`session_01HFAuCZPty5SsvseoPRgukQ`). Declared surface addition:
amendment 6408228005 (one `scripts/check-route-envelope.mjs` row,
below).
## What changes
`GET /i18n/translations/:locale` (and its `?locale=` spelling) answered
every console open with the whole locale bundle and no `ETag` or
`Cache-Control`, so the browser could not revalidate what it already
held. On a hosted tenant that is 80–88 KB compressed per open (the
card's cloud#2914 reading).
1. **`packages/runtime/src/domains/i18n.ts`**, the one handler body
every transport converges on. A new `translationsAnswer` builds the
translations answer on both of the route's success exits (the direct
read and the `zh` → `zh-CN` fallback):
- the `200` is `deps.success`'s envelope, unchanged, plus `ETag` and
`Cache-Control: private, no-cache`;
- the `ETag` is weak, `W/"BASE64URL-SHA256"` over `JSON.stringify` of
the exact envelope served (`locale`, `requestedLocale` and
`translations`), **computed on every read**. Nothing cheaper (a counter,
the bundle object's identity) stands in for the bytes, so a `304` can
only ever pin a caller to the bundle it really holds. It is weak because
it vouches for the JSON, not for the encoding a compressing layer puts
on the wire. This is the invariant `getMetaItemCached` states for
`/meta` (`packages/metadata-protocol/src/protocol.ts`), with SHA-256 in
place of its 32-bit hash;
- a request whose `If-None-Match` names that representation (RFC 9110
§13.1.2: a comma list, weak comparison, or `*`) is answered `{ status:
304, headers }` with no `body`. The `304` carries the same two headers
(§15.4.5). A malformed or stale tag is served the full bundle, the safe
direction;
- `private` because the bundle is served only to a signed-in caller and
names the application's own objects; `no-cache` because the bundle
changes without notice (an authored translation, a package install), so
freshness is the validator's job and never a TTL.
- The anonymous floor (ADR-0056 D2) is still the handler's first
statement: an anonymous caller holding the current validator is refused
`401`, and no refusal carries either header.
2. **No transport change.** `dispatcher-plugin.ts`'s `sendResultBase`
and `@objectstack/hono`'s `toResponse` already write `body: undefined`
as a body-less response (`JSON.stringify(undefined)` is `undefined`, so
Hono builds `Response(undefined, { status: 304 })`). Both are now pinned
on that.
3. **Declared surface addition (amendment 6408228005).**
`scripts/check-route-envelope.mjs` `DISPATCHER_DOMAINS['i18n.ts']` goes
0 → 2 with a note: the two answers are kind 1, the same classification
`meta.ts` took for `Vary: Accept-Language` (`deps.success` takes no
headers, and a `304` has no envelope a helper could build). No other row
and no gate logic moves, and `DomainHandlerDeps` gains no member.
4. **Changeset**: `@objectstack/runtime` patch.
Not touched: `packages/spec`, every published response schema (the `200`
body is byte-identical and `GetTranslationsResponseSchema` is
unchanged), `/i18n/locales`, `/i18n/labels/*`.
The console needs no change: `ui/apps/console/src/loadLanguage.ts` reads
the bundle through `createAuthenticatedFetch`, a plain `fetch(input, {
...init, headers })` with the default cache mode, so the browser sends
`If-None-Match` itself and hands the console the cached body on a `304`.
## Measured on a real composition
`examples/app-crm` booted with `pnpm dev:crm -- --fresh` from this
branch (full `pnpm build`, `VERDICT command-exit 0`), signed in as the
seeded admin, read over loopback with `curl`:
| locale | cold read | read again with the issued `ETag` in
`If-None-Match` |
|:--|:--|:--|
| `zh-CN` | `200`, 220,391 B raw (67,303 B at `gzip -9`; the dev server
does not compress) | `304`, 0 B |
| `en` | `200`, 213,264 B raw (58,351 B at `gzip -9`) | `304`, 0 B |
- Both answers carry `Cache-Control: private, no-cache` and the same
`ETag`; the `200` keeps `Vary: Origin` from the CORS layer.
- 25 cold reads of `zh-CN` answered one `ETag`, and 25 conditional reads
all answered `304`.
- The `ETag` recomputed offline from the served `zh-CN` body equals the
header byte for byte
(`W/"cOAyyUQ7yb5bulH5g7ybiAf8sbJebhwAZur_peIct_E"`): the invariant, read
on the real bundle.
- **Cost.** The extra `JSON.stringify` plus SHA-256 of the 220 KB
envelope is 1.8 ms per read on this 4-core shared box (stringify alone
0.9 ms; mean of 50 after a warm-up). Loopback wall time: `200` median
16.1 ms, `304` median 11.6 ms over 25 reads each. The saving the card
targets is the bytes: on the ops seat's measured link (326–464 KB/s) the
80–88 KB compressed hosted bundle is about 0.2–0.27 s per warm open.
- **Before.** `origin/main` `cb0a2fcf`'s handler sets no header on this
route (the card's measurement on pin `888d6865` shows none, and the
ablation below reds every header assertion on that blob).
## Verification
Commits: `a373155e` (the fix), `e8c4b7a7` (runtime tests), `0b2e6592`
(adapter test, changeset), `7b2a1a2b` (test typing), `773c3f3b` (the
gate row). Each reading names its head.
- **New tests.**
- `packages/runtime/src/domains/i18n-translations-etag.test.ts`, 21
tests on the handler body: the `200` headers on both spellings and the
fallback arm; the validator recomputed from the served body; stable
while unchanged; moves on a one-string change and on an in-place
mutation of the same bundle object; `304` for the weak tag, its strong
spelling, a list with and without spaces, `*`, a Fetch `Headers` object
and a repeated header in the record shape, on both spellings; full
bundle for a stale, unquoted or empty tag; a validator issued before a
change gets the new bundle; one locale's validator does not answer
another locale; the anonymous floor answers `401` with no headers and
the provider unread.
-
`packages/runtime/src/i18n-translations-etag.hono.integration.test.ts`,
4 tests over a real socket (`LiteKernel` + `HonoServerPlugin` +
`createDispatcherPlugin`): `200` with both headers and the same
envelope; `304` with a 0-byte body and both headers; a validator from
before a bundle change is served the new bundle; anonymous with the
validator is `401`, no `ETag`.
- `packages/adapters/hono/src/hono-not-modified-response.test.ts`, 3
tests: a dispatcher `{ status: 304, headers }` leaves `toResponse` as a
0-byte `304` with both headers; the domain is handed the raw `Request`
whose `if-none-match` it reads; the `200` control keeps its JSON body.
Characterization pins: `toResponse` is unchanged, and they were green
before the domain change.
- **Ablation** (fix committed first, `i18n.ts` replaced on disk by its
`origin/main` `cb0a2fcf` blob under a trap, restored with `git checkout
HEAD --`): the two runtime files went 23 failed of 25. The 2 that stay
green are controls that hold either way (the missing-locale `400`
carries no headers; anonymous gets `401`). Restored blob `bb41d094`
equals `HEAD`'s, `git diff HEAD` empty, `git status` clean.
- **Package suites.** At `0b2e6592` (the later commits touch only the
integration test's typing, re-run green at `7b2a1a2b`, and the gate
row): `pnpm --filter @objectstack/runtime test` (vitest `local` project,
under the verify lock, `VERDICT command-exit 0`): 446 files, 7,125
passed, 19 skipped. `pnpm --filter @objectstack/hono test`: 13 files,
228 passed. `typecheck` for both packages exits 0 (runtime's includes
`check:test-typecheck`, which first caught two untyped `res.json()`
reads in the integration test, fixed in 7b2a1a2).
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands` at
head `773c3f3b` derives 87 families on the 6 changed paths, and all 87
exit 0 (each exit code captured before any pipe). `node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`:
87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The first run, at
`7b2a1a2b` (67 families), had three that were not green:
`check-plugin-teardown-shape.mjs --self-test` and
`check:dual-build-cjs-loads` answered PREREQUISITE NOT MET (exit 3: the
shallow clone lacked the fixture commit `621a4876`, and `dist/` was
unbuilt), and both exit 0 after fetching that commit and a full `pnpm
build`; `check:route-envelope` was red on `i18n.ts handBuilt: found 2,
declared 0`, which the declared row above classifies. `pnpm exec eslint
--no-inline-config` on the five changed code files exits 0. `node
scripts/pm/check-governed-merges.mjs --branch HEAD`: 0 of 6 paths hit
the register, NOT governed, 561 changed lines.
## Acceptance notes
- **Out of scope by the claim, carrier `domain:spec`:** the card's
second half, keeping `metadataForms` (46.8 KB raw on the measured
tenant) out of the boot bundle and loading it with Studio, plus the
optional scoping of `objects` to the current app. Both change what `GET
/i18n/translations/:locale` returns, which is a wire-shape change
(triage 6397341079). #25785 stays open for it.
- `/i18n/locales` and `/i18n/labels/:object/:locale` still carry no
validator. Neither was measured on the open path and the card does not
ask for them; noted, not filed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3761710 commit 69fa23d
6 files changed
Lines changed: 558 additions & 3 deletions
File tree
- .changeset
- packages
- adapters/hono/src
- runtime/src
- domains
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
0 commit comments