Repository navigation
Commit 1ac2bb5
perf(metadata-protocol,rest,runtime): /meta/app judges its navigation on the execution face; diagnostics verdicts are memoized by content and schema (#25941)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D7ZazGN9DENufYRJ7p13Wia)_
Closes #25783
Clause-②: yes (widening): the exported `RestProtocol` type of
`@objectstack/rest` gains one optional member,
`getMetaItemsForExecution?`. Measured on the built `dist/index.d.ts` at
`cb0a2fcf` and at `22f886e1`: rest's export list is unchanged (67 names)
and, outside doc comments, that member is the only changed line; the
`@objectstack/metadata-protocol` and `@objectstack/runtime` declarations
are byte-identical.
Metadata-protocol: none
A read-path cost change. No accept set, served body, route parameter or
error code changes, and nothing under `packages/spec` or `ui/` moves.
The one published type that changes is `RestProtocol`, by the optional
member above (changeset
`.changeset/25783-rest-meta-app-execution-face.md`); all three packages
stay `patch`, as `Metadata-protocol: none` requires. The served bytes of
six metadata types were hashed before and after and are identical
(below).
## What changes
**1. `GET /meta/app` judges its navigation on the protocol's execution
face** (`packages/rest/src/rest-server.ts`,
`packages/runtime/src/domains/meta.ts`).
The nav-servability gate (`resolveNavServability`, #24031) reads each
object's `enable` to prune the nav entries whose object refuses `list`.
It read them through `loadObjectItems` → `metaListSource` → the served
`getMetaItems`, which stamps `_diagnostics` on every object, so each
`/meta/app` read ran a full zod `safeParse` of every object definition
only to read `enable`. Measured at 63 of 73 ms CPU per read.
`RestServer.metaListSource` now asks `getMetaItemsForExecution` for
`object` when the protocol has it: the same list, without the serving
decorations (the #24031 / #24535 precedent for `/data`). The
dispatcher's `metaItemReadGateSources.listMetaItems` does the same (seat
amendment 6407727649), so `/meta/app` through the `${prefix}/*`
catch-all gets the same cut. A protocol without the execution face is
asked `getMetaItems` as before. `RestProtocol` gains the optional
`getMetaItemsForExecution` member; `MetaDomainProtocol` gains it in its
existing partial pick of the protocol type. Book and doc lists stay on
the served face (their readers want the served shape).
**2. Diagnostics verdicts are memoized by schema and content**
(`packages/metadata-protocol/src/metadata-diagnostics.ts`).
`computeMetadataDiagnostics` keeps one bounded LRU (4096 entries) per
schema object, held in a `WeakMap`, keyed by a SHA-256 of the stripped
document. The key encodes everything the parse can tell apart: plain
JSON documents key on `JSON.stringify`; a document holding `undefined`
(a strict schema refuses an undeclared key whose value is `undefined`,
and an array hole parses as `undefined`) keys on an encoding that spells
`undefined` out; a document holding anything without a faithful key (a
function, a class instance, `NaN`/`Infinity`, `-0`, nesting past 256) is
parsed every time, as before. A `registerMetadataTypeSchema` replacement
is a new schema object and starts from nothing. Each caller gets its own
copy of the verdict. The inputs are only the schema and the document: no
global zod error map is installed anywhere, and the node-config contract
maps the refinements read are fixed after first build. This is not a
cache of any read's answer; it sits below every merge, on a pure
function of its own input, so a metadata change is a different document
and a different key. `protocol.ts` is untouched (0 lines).
## Measured
Showcase app (`examples/app-showcase`, 108 objects), local, signed in as
the seeded admin. Main-thread CPU per request from
`/proc/PID/task/PID/stat` over 40 serial requests after 5 warm-ups;
shares from a V8 CPU profile at 250 µs sampling over 40 more. Shared box
(4 cores), so read the ratios.
| Endpoint | CPU before (`cb0a2fcf`) | CPU after (`a8f08c97`) |
`computeMetadataDiagnostics` share before → after |
|:--|--:|--:|--:|
| `GET /api/v1/meta/app` | 73.25 ms | 16.75 ms | 59.7% → 1.3% |
| `GET /api/v1/meta/object` | 85.75 ms | 37.00 ms | 50.7% → 15.6% |
| `GET /api/v1/meta/view` | 13.25 ms | 11.25 ms | 11.8% → 3.1% |
Served bytes, SHA-256 prefix of each `GET /api/v1/meta/:type` body,
before vs after on the first read after boot and on a warm read:
| Type | Bytes | Before | After, cold | After, warm |
|:--|--:|:--|:--|:--|
| app | 17,559 | `68ea5cb73ccd` | `68ea5cb73ccd` | `68ea5cb73ccd` |
| object | 694,011 | `3fe564c0fe63` | `3fe564c0fe63` | `3fe564c0fe63` |
| view | 23,954 | `71352ec4f6a6` | `71352ec4f6a6` | `71352ec4f6a6` |
| flow | 52,153 | `d35da2f62bcd` | `d35da2f62bcd` | `d35da2f62bcd` |
| dashboard | 12,075 | `ef3c4c852a3d` | `ef3c4c852a3d` | `ef3c4c852a3d`
|
| page | 115,021 | `bf75d678c85d` | `bf75d678c85d` | `bf75d678c85d` |
## Tests
- `packages/rest/src/meta-app-nav-servability-execution-read.test.ts`
(new): the real REST handlers over the real
`ObjectStackProtocolImplementation` and `SchemaRegistry`, with the
`object` schema behind a recording proxy (the
`rest-exposure-gate-execution-read.test.ts` instrument). Unscoped and
environment-scoped: the app list and the app by name prune the
unlistable entry and keep the listable one with **zero** object
diagnostics; FALLBACK, a protocol without the execution list face, gets
the same prune and diagnoses every object (so the zeros are not
vacuous); CONTROL, `GET /meta/object` still serves `_diagnostics` on
every object. On `cb0a2fcf`'s `rest-server.ts` the four zero rows fail
(`expected [ 'listed_widget', 'unlisted_widget' ] to deeply equal []`).
-
`packages/runtime/src/domains/meta-app-nav-object-execution-face.test.ts`
(new): through `HttpDispatcher.dispatch()`, `/meta/app` and
`/meta/app/:name` read `object` on `getMetaItemsForExecution` and never
on `getMetaItems`, with the same prune; without the execution face it
falls back to `getMetaItems`. The two execution-face rows fail on
`cb0a2fcf`'s `meta.ts`.
- `packages/metadata-protocol/src/metadata-diagnostics.memo.test.ts`
(new): one parse per document per schema, a structurally equal copy and
the decorated served body included; a changed document, a replaced
schema each re-parse; a missing key vs the key holding `undefined`,
`null` vs an array hole, `null` vs `NaN` each get their own verdict in
both orders; a function, `-0`, a 300-deep nesting and a `Date` parse
every time; callers get independent copies; the 4096 LRU bound evicts
the least recently used. On the real `object` schema, a memo hit serves
byte-identical `decorateMetadataItem` output to a fresh parse, for a
valid and an invalid document. Mutation check: with a naive
`JSON.stringify` key all six collision rows fail.
- `packages/rest/src/rest-exposure-gate-execution-read.test.ts`
(re-pinned): its recording proxy is now registered per test
(`beforeEach`), because verdicts are memoized per schema object; five
rows recorded zero parses under one shared proxy.
Full suites: `@objectstack/metadata-protocol` 264 files / 21,505 tests,
`@objectstack/rest` 328 / 6,730, `@objectstack/runtime` 448 / 8,011, all
green. `typecheck` green for the three packages (test layers included
for rest and runtime).
## Gates
`node scripts/pm/dispatch-gates.mjs --commands` derives 71 commands for
this diff; all 71 ran with exit 0, and `--ran` reconciles 71 run, 0
NOT-MEASURED, 0 UNRUN. Four were red on the first pass and green after:
- `check-rest-log-spy-declared`: the new rest test spied on
`console.warn` without declaring `OS_REST_LOG`. The spies asserted
nothing, so they are gone.
- `check:engine-double-contract`: the new rest test carried a `findOne`
double no path reached. Dropped, so the pinned ledger is unchanged.
- `check-plugin-teardown-shape --self-test`: shallow clone missing the
pinned fixture commit; fetched it.
- `check:dual-build-cjs-loads`: prerequisite, seven packages had no
`dist/`; built them.
`pnpm lint` green.
## Acceptance notes
- **`GET /meta/app` lands at 16.75 ms against the card's ≤15 ms.** What
remains is not diagnostics (1.3%): the profile puts about 4.4 ms in
artifact resolution (`lookupArtifactItem` / `foldExtenders`) and about
7.8 ms in fixed per-request overhead (`(program)`, routing, auth).
Neither is on this claim's file surface. The maintainer relaxed both
targets to these numbers (ruling 6408692677, letter A).
- **`GET /meta/object` lands at 15.6% in `computeMetadataDiagnostics`
against the card's ≤5%.** Warm reads run no zod parse at all; the
remaining 9.3 ms is the content key itself, one `JSON.stringify` plus
SHA-256 over 694 KB of definitions (about 6 ms in an isolated bench) and
the shape walk. A content key costs about one serialization, so content
keying cannot reach 5%. The card's other route does: Studio opts in to
`_diagnostics` with a query switch, and plain `/meta/object` reads skip
them. That changes `/meta`'s parameters and the served body for every
caller that does not opt in, and about a dozen `_diagnostics` readers in
`ui/` would have to send the switch: a contract change, outside this
claim (claim 6407697525). Reported to the seat as a finding; not
dispatched (ruling 6408692677).
- **Deviation:** the before/after servers were started with `pnpm
--filter @objectstack/example-showcase dev -- --fresh -p 38425` rather
than `pnpm dev`, because `check-dev-prereqs` refused `pnpm dev` with 45
unbuilt packages in this worktree. The same command and port served both
sides.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019d4JoyFgaZCRubmNWh91uZ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 69fa23d commit 1ac2bb5
10 files changed
Lines changed: 712 additions & 6 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 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 | + | |
0 commit comments