Skip to content

perf: chunked module building, and component $ref fixes - #30

Merged
maxholman[bot] merged 8 commits into
masterfrom
perf/batch-statements
Sep 27, 2026
Merged

maxholman[bot] merged 8 commits into
masterfrom
perf/batch-statements

Conversation

@maxholman

@maxholman maxholman Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Makes fixture generation roughly 4–8× faster and fixes the component $ref failures ("ref used before available", "Cyclic dependency"). Output for existing documents is byte-identical.

Speed

9352976 builds types.ts, commands.ts and valibot.ts in 10-statement scratch files and joins them once, instead of inserting into the whole module. ts-morph re-parses the entire file on every insert, so generation was quadratic in module size.

fixture before after
openai 59s 7.5s
docker 48s 17s

$ref fixes

  • f58b248: dependencies come from subschemas only. A $ref inside example/default/const/enum data no longer creates a fake edge or cycle; a property named default still counts.
  • 7bd100f, 483058c: a $ref to an undefined schema is rejected up front with the JSON pointer of the ref (component or operation), instead of "ref used before available".
  • 29e493d: escaped (pet~1kind), percent-encoded (Pet%20Kind) and partial (…/Address/properties/postcode, …/$defs/X) refs resolve. It also fixes schema names with / or ~ casing into invalid identifiers (Pet~tag, inputPet/kindSchema).
  • eca5933: recursive schemas (self and mutual) generate. Types name the later alias, and valibot reads it with v.lazy((): v.GenericSchema<unknown, T> => …). Input schemas use UndefinedOnPartialDeep<T>. The generated output typechecks under strictest (exactOptionalPropertyTypes), including coerced wire types through a cycle.

Optional

7c21f7d drops @apidevtools/json-schema-ref-parser for a local RFC 6901 resolver over the in-memory document. Its resolve() read other files and URLs at codegen time; non-local refs are now refused by name. The commit stands alone and can be dropped.

Verification

  • All four fixtures regenerate byte-identical to 124102c, warnings included. The chunk ordering was also stress-tested at chunk size 1.
  • New __tests__/refs.test.ts: 12 tests with whole-file snapshots or rejects.toThrow.
  • tsc over the generated refs snapshots passes under @block65/tsconfig/nodejs26.
  • oxfmt, oxlint (lib and type-aware), and fallow are clean.
  • vitest: 87/87 tests pass. test1.test.ts fails as on the base, because it needs rest-client 16 (EventStreamCommand).

maxholman Bot and others added 8 commits September 27, 2026 19:50
ts-morph re-parses the whole source file on every insert, so emitting
types.ts, commands.ts and valibot.ts one statement at a time was
quadratic in their size. Their statements now go into scratch files of
ten and join each module once it is complete, and each command class
trims its default output argument while it is still small.

A body alias declared inside the arguments of the alias that unions
them could land in the next chunk, after it, so those are declared
first. Output is byte-identical: openai generates in 8s rather than
59s, docker in 17s rather than 48s.

Co-Authored-By: LLM <noreply@block65.dev>
getDependents collected every $ref anywhere under a schema, including
inside example, default, const and enum values. A $ref-shaped key in
sample data became a dependency, and could close a cycle that failed the
component sort. It now walks only the keywords that hold subschemas, so
a property named default or example still counts.

Co-Authored-By: LLM <noreply@block65.dev>
A component $ref to a schema the document does not define failed later
as "ref used before available", the message of an ordering bug. The
component sort now rejects it first, naming the schema and the ref.

Co-Authored-By: LLM <noreply@block65.dev>
Schemas registered under `#/components/schemas/<name>` as written, so a
$ref that escaped the name as a JSON pointer does (`~1` for `/`, `~0`
for `~`), percent-encoded it, or pointed into part of a schema found
nothing and failed as "ref used before available".

The document's local $refs are now normalized before generation. A ref
is percent-decoded, a component registers under its escaped name, and a
ref into part of a component is replaced by that subschema, keeping any
keywords beside the $ref. A ref into its own schema or to a missing
part is refused by name.

Such names also cased into invalid identifiers (`Pet~tag`,
`inputPet/kindSchema`), so casing now splits on every character an
identifier cannot hold, and valibot names use the same helper.

Co-Authored-By: LLM <noreply@block65.dev>
Only refs between component schemas were checked, so an operation's
body, response or parameter that referred to a missing schema still
failed as "ref used before available". Every component schema $ref in
the document is now checked as the refs are normalized, and the error
gives the JSON pointer of the ref.

Co-Authored-By: LLM <noreply@block65.dev>
A schema that contains itself, or schemas that refer to each other,
failed the component sort with "Cyclic dependency". No order can satisfy
a cycle, so the sort now drops the ref that closes each loop, and
generation no longer needs the order for such refs.

A type alias may name one declared later, so a type ref to a component
not registered yet uses the name that component will register under.
Validators are named for every component before any is emitted, and a
ref to one not emitted yet reads it with v.lazy. The lazy getter is
annotated with the generated type, imported from the types module,
which stops TypeScript inferring the schema from itself. Only the output
type is given, since a wire schema coerces its input.

Acyclic documents generate byte-identical output.

Co-Authored-By: LLM <noreply@block65.dev>
Codegen used the ref parser only to look up refs inside the document it
was given, but resolve() also followed refs to other files and URLs,
reading and fetching them at codegen time. A ref is now looked up in the
in-memory document with the same JSON pointer decoding the schema
registry uses, and a ref outside it is refused by name.

Optional: this commit stands alone and can be dropped.

Co-Authored-By: LLM <noreply@block65.dev>
A misspelled keyword now fails typecheck instead of silently never
matching. getDependents reads each keyword through the schema's own types.
additionalItems and definitions are dropped: OAS 3.2 does not define them.

Co-Authored-By: LLM <noreply@maxholman.dev>
@maxholman
maxholman Bot force-pushed the perf/batch-statements branch from 7c21f7d to c746e50 Compare September 27, 2026 12:28
@maxholman
maxholman Bot changed the base branch from feat/event-stream-output to master September 27, 2026 12:29
@maxholman maxholman Bot closed this Sep 27, 2026
@maxholman maxholman Bot reopened this Sep 27, 2026
@maxholman
maxholman Bot merged commit e76ff27 into master Sep 27, 2026
1 check passed
@maxholman
maxholman Bot deleted the perf/batch-statements branch September 27, 2026 13:17
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.

0 participants