Repository navigation
perf(lazy-zod): add internalZ, which also builds leaf schemas on first use - #1011
Conversation
…t use An internal lazy z for FrontMCP's own schemas: every factory is deferred, leaves included, and leaves are built with their parent. Built FrontMCP schemas skip Zod 4.6's first-parse cycle walk (user schemas keep it). The public z stays Zod's own. The sweep that moves sdk, auth and utils onto it follows in the next pull request.
|
Warning Review limit reachedYour organization has reached its usage spending cap. Adjust your spending cap in the billing tab. Next included review available in 45 minutes. View limit details
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Lazy schema build lifecycle libs/lazy-zod/src/lazy-z.ts, libs/lazy-zod/src/lazy-schema.ts, libs/lazy-zod/src/memoizer.ts |
createLazyZ accepts configurable factory names and a stand-in callback. Lazy schema construction uses memoizer wrappers and clears its materialization factory after building. |
Internal Zod factories and child schemas libs/lazy-zod/src/internal-z.ts, libs/lazy-zod/src/factories.ts, libs/lazy-zod/src/index.ts, libs/lazy-zod/src/utils.ts, libs/lazy-zod/README.md |
internalZ defers configured factories, builds eligible children, and exposes Zod type aliases. Deep materialization covers additional child-definition keys. The package exports and documents internalZ. |
Schema behavior and materialization tests libs/lazy-zod/src/__tests__/internal-z.spec.ts, libs/lazy-zod/src/__tests__/materialization.spec.ts |
Tests compare internalZ and Zod across factory and chain behavior, parsing, JSON Schema, memoizer handling, and inferred types. Materialization tests compare lazy and eager merged schema JSON output. |
Estimated code review effort: 3 (Moderate) | ~25 minutes
Sequence Diagram(s)
sequenceDiagram
participant Caller
participant internalZ
participant InternalLazySchema
participant Zod
Caller->>internalZ: call a schema factory
internalZ-->>Caller: return a schema or stand-in
Caller->>InternalLazySchema: parse or invoke a chained method
InternalLazySchema->>Zod: build the deferred schema
Zod-->>Caller: return the parse result
Possibly related issues
- 1.10: build FrontMCP's own zod schemas on first use #949: This issue calls for an internal lazy-zod export that defers leaf schemas while keeping public
zunchanged. This PR addsinternalZand tests the publiczbehavior. - 1.10: start with less live heap, so V8 doesn't double the young generation #948: This issue identifies retained Zod schemas as a contributor to heap growth. This PR adds deferred schema construction.
Possibly related PRs
- agentfront/frontmcp#981: Both PRs concern lazy-schema JSON Schema materialization. This PR extends materialization to
catchall,rest,in, andout.
Suggested reviewers: alexmercerpo
Merge Risk: 
Minimal · previous review 99f9c
-
This PR adds an internal lazy schema builder without changing the public
zbehavior. No actionable merge-blocking issue was found. Memory savings are measured in the follow-up PR that adoptsinternalZ, not here.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Description check
-
Explanation
The description covers the 'What and why' and 'Tests' sections with substantive content. It explains the motivation (issue
#949: about 6.3 MB of zod schema instances retained after startup, with leaf schemas created at module load and a first-parse cycle walk building 440 of 947 lazy schemas), the new internalZ building block, the memoizer swap, and the public z behavior. The Tests section lists the spec files, what each covers, and the coverage numbers. It also adds a Bundle size section that is not in the template; it is useful context. The 'User-facing change' section is present and states 'None in any public API', with a reason. Remaining gaps are minor: the 'User-facing change' template bullets (frontmcp.dev pages affected, Skills catalog entries) are not addressed, and the 'Tests' section does not say whether any checks were run by hand. These gaps are non-critical, so the description is mostly complete.
-
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 23.08% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 13 functions across 9 files. (1 skipped: 1 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches
-
Generate docstrings
-
- Commit to this branch
- Create a new PR
Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
|
@coderabbitai review |
…perf/p03a-internal-z
Action not completed
|
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-02T17:40:40.214Z |
…perf/p03a-internal-z
lazy-zod's browser build now carries internalZ and its memoizer, so create() + one tool grows by 2,180 bytes minified and create() + WebMcpPlugin + one tool by 2,181. The two budgets move up by exactly that, keeping the headroom they had.
|
@coderabbitai review |
Action not completed
|
Stacked on #1007
Refs #949 (part 1 of 2; the sweep that uses it is #1010, which closes the issue)
What and why
#949: after startup a FrontMCP server holds 6.3 MB of zod schema instances. The public lazy
zdefers compound schemas, but leaf schemas (z.string(),z.enum(), …) are created when their module loads, and Zod 4.6's first-parse cycle walk builds 440 of the 947 lazy schemas during startup anyway.This PR adds the building block only, split out of #1010 so each PR stays under CodeRabbit's 150-file limit:
internalZin@frontmcp/lazy-zod: every factory is deferred, leaves included. Leaves are built together with their parent, so a built schema holds real Zod leaves and parsing doesn't go through proxies.memoizer.ts), swapped in for the build and restored infinally. A user stand-in built inside that window gets Zod's own memoizer back; the marker is aSymbol.forregistry symbol so two loaded copies of lazy-zod agree.zunchanged. Tests pin that its leaf factories are Zod's own. Two internal changes also touch it: stand-ins share one proxy handler and drop their build closure once built, andforceMaterializealso followscatchall,restand pipein/out(written withReflect.set, so.merge()d schemas convert withtoJSONSchema).User-facing change
None in any public API.
internalZis internal: the lazy-zod README says so and@frontmcp/sdkdoesn't re-export it.Tests
libs/lazy-zod/src/__tests__/internal-z.spec.ts(167): pins the publicz; table-driven parity for every factoryinternalZdefers (inputs, issues, messages, and JSON Schema compared with Zod's owntoJSONSchema); the memoizer swap and its restore; user stand-ins built inside the swap; two lazy-zod copies; the Zod internals the swap relies on, and the fallback when Zod has none.materialization.spec.ts:catchall,rest, pipes, and.merge()chains throughtoJSONSchema.internal-z.tsandmemoizer.ts100%); utils, auth and sdk suites anddemo-e2e-bundle-sizepass on this branch.The measurements (zod instances 6.32 → 4.14 MB retained, heap used −3.6 MB) come with #1010, which moves the schemas onto
internalZ.Summary by CodeRabbit
zinterface backed by Zod.Bundle size
lazy-zod's browser build now carriesinternalZand its memoizer. Measured on this branch against #1007 (both on today'smain):create()+ one tool grows by 2,180 bytes minified (1,319,959 → 1,322,139) andcreate()+ WebMcpPlugin + one tool by 2,181 (1,327,146 → 1,329,327). The two browser budgets indemo-e2e-bundle-sizemove up by exactly that (1,321,639 → 1,323,819 and 1,328,515 → 1,330,696), keeping the headroom they had.