Skip to content

perf(admin): load the meta schema on first request, not at boot - #554

Open
JonasJesus42 wants to merge 1 commit into
mainfrom
feat/lazy-meta-schema
Open

JonasJesus42 wants to merge 1 commit into
mainfrom
feat/lazy-meta-schema

Conversation

@JonasJesus42

@JonasJesus42 JonasJesus42 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Problema

createAdminSetup documenta a opção meta como "Lazy loader for admin meta schema — only fetched when admin requests it". E aí, na linha 47:

// 7. Admin meta schema (lazy)
options.meta().then((data) => setMetaData(data));

Isso roda no boot de todo isolate. O schema inteiro — JSON Schema de cada seção e cada app — é importado, passa por composeMeta, e o resultado fica pinado em globalThis.__deco_meta_data pela vida do isolate. Para tráfego que nunca vai tocar /live/_meta, que é a única rota que lê isso.

É o bug do decofile numa segunda casa: grafo JSON grande permanentemente alcançável por um binding de módulo. O próprio plugin.js:570 já documentava o sintoma ("setup.ts imports meta.gen.json EAGERLY via createAdminSetup") sem tratá-lo como problema.

Mudanças

Lazy de verdade. createAdminSetup registra o loader (setMetaLoader); o primeiro /live/_meta resolve. Requests concorrentes compartilham um único load em voo. Falha não latcha — responde 503 e o próximo request tenta de novo, senão uma falha transitória de chunk-load desabilitaria o admin pela vida do isolate. setMetaData continua funcionando para quem seta explícito, e a invalidação só descarta o grafo quando existe loader para reconstruir (descartar sem loader daria 503 permanente).

meta.gen.json vira JSON.parse("...") no SSR, mesmo tratamento do blocks.gen. O parser JSON do V8 é o caminho rápido, mas o motivo aqui é memória: o schema fica como uma string até alguém chamar o parse — então um isolate que nunca serve admin nunca materializa o grafo.

Quebra que isso causa

handleMeta vira async. Todos os callers já esperavam promise, exceto uma linha em packages/nextjs/src/routeHandlers.ts:60 — corrigida aqui, e só isso foi tocado no Next. O tipo da interface em workerEntry.ts:222 alarga para Response | Promise<Response>, senão um site passando o handler real quebraria no typecheck.

Testes

10 casos. O que fixa a propriedade que interessa: o loader não é chamado no setup. Mais: chamado uma vez no primeiro request e só; um load compartilhado entre requests concorrentes; o schema servido é o composto (framework, que é o próprio sentinel do composeMeta); 503 → retry após falha; setMetaData explícito sobrevive à invalidação; recomposição após invalidação quando há loader; 304 com If-None-Match.

🤖 Generated with Claude Code


Summary by cubic

Loads the admin meta schema on the first /live/_meta request instead of at boot. Previously createAdminSetup invoked the meta thunk during setup, so every isolate imported the full schema, composed it, and pinned the result on globalThis for its lifetime — even for traffic that never serves the admin.

Now setup registers the loader, and the first /live/_meta request resolves it. Concurrent first requests share one in-flight load; a failed load answers 503 and retries on the next request without latching; and explicitly-set data via setMetaData still survives invalidation. The Vite SSR plugin also emits meta.gen.json as JSON.parse("..."), keeping the schema as one string until something calls the parse.

handleMeta becomes async: the only non-awaited caller, in packages/nextjs/src/routeHandlers.ts, is updated, and AdminHandlers widens to accept Response | Promise<Response>.

Written for commit bd86943. Summary will update on new commits.

Review in cubic

`createAdminSetup` documented its `meta` option as "Lazy loader for admin
meta schema — only fetched when admin requests it", and then invoked the
thunk at module init. So every isolate, on boot, imported the schema, ran
composeMeta over it, and pinned the composed graph on globalThis for its
whole life — for traffic that will never touch `/live/_meta`. The schema
covers every section and every app; exactly one route reads it.

This is the decofile bug in a second place: a large JSON graph made
permanently reachable from a module-level binding.

`createAdminSetup` now registers the loader (`setMetaLoader`) and the
first `/live/_meta` resolves it. Concurrent first requests share one
in-flight load. A failed load is not latched — it answers 503 and the next
request retries, so a transient chunk-load failure can't disable the admin
for the isolate's life. `setMetaData` still works for sites that set the
schema explicitly, and invalidation only drops the cached graph when a
loader exists to rebuild it (clearing it otherwise would 503 permanently).

Second half: the vite plugin now emits `meta.gen.json` as
`JSON.parse("...")` on SSR, the same treatment blocks.gen gets. V8's JSON
parser is the faster path, but the reason here is memory — the schema
stays ONE string until something calls the parse, so an isolate that never
serves the admin never materializes the object graph at all.

`handleMeta` becomes async. Every caller already awaited it except one
line in the Next route handlers, fixed here; the worker-entry interface
type widens to `Response | Promise<Response>` so a site passing the real
handler still typechecks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JonasJesus42
JonasJesus42 requested a review from a team September 14, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant