Skip to content

🎉 feat: specialize response mapping via AOT for known schema types#1799

Open
DenisCDev wants to merge 2 commits intoelysiajs:mainfrom
DenisCDev:feat/specialize-response-aot
Open

🎉 feat: specialize response mapping via AOT for known schema types#1799
DenisCDev wants to merge 2 commits intoelysiajs:mainfrom
DenisCDev:feat/specialize-response-aot

Conversation

@DenisCDev
Copy link

@DenisCDev DenisCDev commented Mar 10, 2026

Summary

When a route declares a response schema (e.g. response: t.Object({...})), the return type is already known at compile time. Yet after validation, the response still goes through the generic mapResponse/mapCompactResponse chain which performs 10+ type checks (constructor.name switch, instanceof for Response, Blob, ReadableStream, Error, Promise, etc.).

This PR extends the existing Sucrose AOT compilation to the response path — generating specialized inline response code that bypasses the generic dispatch when the schema Kind is known.

What changed

  • getResponseSchemaKind() in compose.ts — detects TypeBox schema Kind (Object, Array, String, Number, Boolean) for single-status-code response schemas
  • specializedResponse() adapter method in adapter/types.ts — lets each adapter generate optimized inline code per type
  • Bun adapter — uses Response.json() for Object/Array (native fast path), new Response() for primitives
  • Web Standard adapter — uses JSON.stringify() + content-type header for Object/Array
  • mapResponse closure in compose.ts — emits a type guard with fast path + fallback to generic

Why it helps

  • Eliminates 10+ conditional branches on the hot path for typed routes
  • Creates monomorphic call sites that V8/JSC can optimize with inline caches (the generic path is megamorphic)
  • Completes the partial evaluation pattern Elysia already applies to the request path (Sucrose) — now the response path is specialized too

Safety

Every specialized path has a runtime type guard that falls back to the generic mapResponse if the value doesn't match:

r !== null && r !== undefined && r.constructor === Object
  ? Response.json(r)        // fast path
  : mapResponse(r, c.set)   // fallback

Specialization is disabled when:

  • No response schema declared
  • Multiple status codes in schema
  • Union/Intersect types (Kind not whitelisted)
  • mapResponse hooks are present (could change type)
  • maybeStream is true (generators/streams)
  • Standard Schema provider (no Kind property)

Test plan

  • All 1508 existing tests pass (bun test) — zero regressions
  • Benchmark typed vs untyped routes with autocannon / bun bench
  • Inspect generated fnLiteral to verify inline Response.json() appears

Summary by CodeRabbit

  • Refactor
    • Introduced optimized fast-path response handling across adapters and core composition to return JSON, text, number, or boolean responses more directly, improving runtime routing and performance while preserving existing behavior and fallbacks.
    • Adapter interfaces extended to optionally support these specialized response paths.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

Walkthrough

Added an optional adapter hook specializedResponse and composer logic to detect single-status-code response schemas (Object/Array/String/Number/Boolean) and generate fast-path, type-aware response code with a guarded fallback to the existing generic mapResponse path.

Changes

Cohort / File(s) Summary
Adapter Interface Definition
src/adapter/types.ts
Introduce optional `specializedResponse(kind: string, r: string, hasSet: boolean, saveResponse: string) => string
Adapter Implementations
src/adapter/bun/index.ts, src/adapter/web-standard/index.ts
Implement specializedResponse in both adapters. Emit type-aware response code: Object/Array → JSON response (with conditional c.set), String → text/plain or raw, Number/Boolean → coerced string, else return undefined.
Composer Integration
src/compose.ts
Add private getResponseSchemaKind(validator) helper and integrate specialization: detect single-status-code schema kinds, build a runtime guard for the kind, and emit conditional fast-path calling adapter.specializedResponse(...) with fallback to existing mapResponse dispatch.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Request
    participant Composer as Composer
    participant Validator as Schema Validator
    participant Adapter as Adapter
    participant Runtime as Runtime Response

    Client->>Composer: invoke composeHandler(route)
    Composer->>Validator: getResponseSchemaKind(validator)
    Validator-->>Composer: kind or null

    alt specialization available
        Composer->>Adapter: specializedResponse(kind, r, hasSet, saveResponse)
        Adapter-->>Composer: specialized code (string) or undefined
        Composer->>Composer: emit guard for kind
        Composer->>Runtime: fast-path conditional -> specialized code
    else fallback
        Composer->>Runtime: emit generic mapResponse dispatch
    end

    Runtime-->>Client: HTTP response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I dug a fast little tunnel through the code,
Found schema carrots in a neat row,
When kinds align I skip the long road,
Quick hops deliver responses aglow,
Nibble the bytes — then off I go!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding AOT specialization for response mapping for known schema types, which is the core feature across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/adapter/web-standard/index.ts`:
- Around line 22-28: In specializedResponse (the switch branch handling 'Object'
and 'Array') the hasSet=true path returns new Response(JSON.stringify(...),
c.set) without ensuring a content-type header; change that path to merge
c.set.headers with {'content-type':'application/json'} when not already present
(i.e., create a headers object combining c.set.headers and the content-type
fallback) so the Response always includes application/json; update the hasSet
branch that references saveResponse, r, and c.set to build a merged init before
calling new Response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8515709-e8cc-41d0-92b9-2270d3ba7e6b

📥 Commits

Reviewing files that changed from the base of the PR and between 169e54a and b1f506d.

📒 Files selected for processing (4)
  • src/adapter/bun/index.ts
  • src/adapter/types.ts
  • src/adapter/web-standard/index.ts
  • src/compose.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/adapter/web-standard/index.ts (1)

22-41: Verify BunAdapter parity for content-type behavior.

The BunAdapter (lines 224-244 in bun/index.ts) relies on Response.json() which auto-sets content-type: application/json per the Fetch spec, and doesn't set explicit headers for String/Number/Boolean. This means the two adapters will produce different content-type headers for the same response types:

Kind WebStandard (hasSet=true) Bun (hasSet=true)
Object/Array application/json ✓ application/json (via Response.json) ✓
String text/plain ✓ (none from adapter)
Number/Boolean (none) (none)

If this divergence is intentional, consider documenting it. If not, consider aligning both adapters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adapter/web-standard/index.ts` around lines 22 - 41, The WebStandard
specializedResponse currently sets 'content-type:text/plain' for String
responses when hasSet=true, which diverges from the BunAdapter that relies on
Response.json() for objects/arrays and does not set text/plain for strings; to
align behavior, update specializedResponse (function specializedResponse) to
stop adding 'content-type:text/plain' for kind==='String' when hasSet is true
(i.e., treat String the same as Number/Boolean: do not force a content-type), or
alternatively add the explicit text/plain header in the Bun adapter where
Response is created—pick one approach and make both adapters consistent (update
the String branch in specializedResponse and mirror the change in the BunAdapter
Response creation logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/adapter/web-standard/index.ts`:
- Around line 33-37: The Number/Boolean branch returns a Response with c.set as
init but doesn't ensure a content-type when hasSet is true; update the 'Number'
and 'Boolean' case in the switch (the code that currently returns `new
Response(${saveResponse}''+${r},c.set)` when hasSet) to merge or override
c.set.headers to include 'content-type': 'text/plain' (same as the String
branch) before passing it to new Response, so that when hasSet is true the
Response init always contains a text/plain content-type; reference the variables
hasSet, saveResponse, r, c.set, and the Response constructor in your change.

---

Nitpick comments:
In `@src/adapter/web-standard/index.ts`:
- Around line 22-41: The WebStandard specializedResponse currently sets
'content-type:text/plain' for String responses when hasSet=true, which diverges
from the BunAdapter that relies on Response.json() for objects/arrays and does
not set text/plain for strings; to align behavior, update specializedResponse
(function specializedResponse) to stop adding 'content-type:text/plain' for
kind==='String' when hasSet is true (i.e., treat String the same as
Number/Boolean: do not force a content-type), or alternatively add the explicit
text/plain header in the Bun adapter where Response is created—pick one approach
and make both adapters consistent (update the String branch in
specializedResponse and mirror the change in the BunAdapter Response creation
logic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2af1589-cf9b-4af1-a7f2-a0c0bec76301

📥 Commits

Reviewing files that changed from the base of the PR and between b1f506d and 0f18be8.

📒 Files selected for processing (1)
  • src/adapter/web-standard/index.ts

Comment on lines +33 to +37
case 'Number':
case 'Boolean':
return hasSet
? `new Response(${saveResponse}''+${r},c.set)`
: `new Response(${saveResponse}''+${r})`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing content-type header for Number/Boolean when hasSet is true.

The String case (lines 30-31) sets content-type: text/plain when hasSet is true, but Number/Boolean does not. When c.set is passed as the Response init, the default content-type behavior is overridden—if c.set.headers lacks a content-type, the response will have none.

This inconsistency could cause clients to misinterpret numeric or boolean responses when headers/cookies/status are active.

🐛 Proposed fix to add content-type for Number/Boolean
 case 'Number':
 case 'Boolean':
     return hasSet
-        ? `new Response(${saveResponse}''+${r},c.set)`
+        ? `(c.set.headers['content-type']||(c.set.headers['content-type']='text/plain'),new Response(${saveResponse}''+${r},c.set))`
         : `new Response(${saveResponse}''+${r})`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adapter/web-standard/index.ts` around lines 33 - 37, The Number/Boolean
branch returns a Response with c.set as init but doesn't ensure a content-type
when hasSet is true; update the 'Number' and 'Boolean' case in the switch (the
code that currently returns `new Response(${saveResponse}''+${r},c.set)` when
hasSet) to merge or override c.set.headers to include 'content-type':
'text/plain' (same as the String branch) before passing it to new Response, so
that when hasSet is true the Response init always contains a text/plain
content-type; reference the variables hasSet, saveResponse, r, c.set, and the
Response constructor in your change.

@DenisCDev
Copy link
Author

The generic mapResponse handler for Number/Boolean in web-standard also does new Response(response.toString(), set) without setting content-type (handler.ts L110-115). The specialized path matches the existing behavior exactly, so no change needed here.

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