Skip to content

Pluggable server-listener seam + a Bun.serve backend #511

Description

@vivek7405

Problem

startServer (the webjs dev / start convenience) is hard-wired to node:http: in dev.js it does createHttp1Server(handler) and bridges every request toWebRequest(nodeReq) -> handle(webReq) -> sendWebResponse(nodeRes, webResponse). After #508, webjs runs on Bun, but only through Bun's node:http COMPATIBILITY layer, which has translation overhead and still pays the IncomingMessage/ServerResponse bridge cost.

createRequestHandler already returns a runtime-agnostic handle(req): Response, and Bun.serve({ fetch: handle }) takes exactly that shape. So on Bun the right server is Bun.serve (native HTTP, Request/Response-native), which also lets us DROP the toWebRequest / sendWebResponse bridge entirely on that path. The benchmark in #508 showed Bun's Request/Response handling roughly 3x faster than Node at the handler level; native Bun.serve would let that flow through to the actual listening server instead of being eaten by the compat layer.

Design / approach

A pluggable server-listener adapter seam, the same factoring Remix 3 uses (its node-fetch-server package's createRequestListener). createRequestHandler stays the runtime-neutral core; startServer detects the runtime (process.versions.bun) and picks an adapter:

  • Bun: Bun.serve({ fetch: handle, websocket }), Request/Response-native, no node req/res bridge.
  • Node: the existing node:http shell (unchanged).

The shared, runtime-NEUTRAL logic must live in a core both adapters call so the two shells cannot drift: the WS dispatch to route.ts WS exports, the SSE live-reload client registry, the #237 server timeouts, the access log, and the proxy-IP / trustProxy resolution (Bun exposes server.requestIP(req) where node:http reads socket.remoteAddress). The CLI auto-selects the adapter so bun --bun run start gets the fast path with no user action.

Gate the work on a benchmark first. The #508 3x number was measured at handle() directly, with NO HTTP server on either side. The number that actually justifies this is node:http-compat-on-Bun vs Bun.serve on the FULL listening path (req/s). Prototype webjs start both ways on Bun and measure; if the delta is large, ship the adapter, if modest, it can wait.

This seam is valuable independent of Bun: it also sets up future Deno.serve and a cleaner embedded-host adapter.

Follow-up to #508 (runtime support) and #509 (Bun test matrix).

Acceptance criteria

  • A benchmark comparing webjs start on Bun via node:http compat vs Bun.serve on the full listening path (req/s), recorded in the PR, justifies (or defers) the adapter.
  • A server-listener adapter seam: createRequestHandler unchanged; startServer selects a node:http adapter (Node) or a Bun.serve adapter (Bun) at runtime.
  • The Bun adapter preserves EVERY node:http-shell feature: the WS upgrade (to route.ts WS exports), SSE live-reload, request/response body-size limits + server timeouts (Harden request ingress: body-size limit (413) and server timeouts #237), compression, the access log + X-Request-Id, proxy-IP / trustProxy, and 103 Early Hints (or it is documented as not-supported-on-Bun with a reason if Bun.serve cannot emit them).
  • Shared logic lives in a runtime-neutral core, not duplicated per shell.
  • bun --bun run start / dev auto-selects the Bun adapter; bun test/bun/smoke.mjs and an e2e-style probe pass against the Bun-served app.
  • Tests cover the adapter selection + the Bun-served WS / SSE / streaming paths.
  • Docs updated: agent-docs/deployment.md, packages/server/AGENTS.md (the dev.js / startServer entry), and the scaffold's Bun-run note.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions