You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.tsWS 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.tsWS 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.
Problem
startServer(thewebjs dev/startconvenience) is hard-wired tonode:http: indev.jsit doescreateHttp1Server(handler)and bridges every requesttoWebRequest(nodeReq)->handle(webReq)->sendWebResponse(nodeRes, webResponse). After #508, webjs runs on Bun, but only through Bun'snode:httpCOMPATIBILITY layer, which has translation overhead and still pays theIncomingMessage/ServerResponsebridge cost.createRequestHandleralready returns a runtime-agnostichandle(req): Response, andBun.serve({ fetch: handle })takes exactly that shape. So on Bun the right server isBun.serve(native HTTP, Request/Response-native), which also lets us DROP thetoWebRequest/sendWebResponsebridge entirely on that path. The benchmark in #508 showed Bun'sRequest/Responsehandling roughly 3x faster than Node at the handler level; nativeBun.servewould 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-serverpackage'screateRequestListener).createRequestHandlerstays the runtime-neutral core;startServerdetects the runtime (process.versions.bun) and picks an adapter:Bun.serve({ fetch: handle, websocket }), Request/Response-native, no node req/res bridge.node:httpshell (unchanged).The shared, runtime-NEUTRAL logic must live in a core both adapters call so the two shells cannot drift: the
WSdispatch toroute.tsWSexports, the SSE live-reload client registry, the #237 server timeouts, the access log, and the proxy-IP /trustProxyresolution (Bun exposesserver.requestIP(req)where node:http readssocket.remoteAddress). The CLI auto-selects the adapter sobun --bun run startgets 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 isnode:http-compat-on-Bun vsBun.serveon the FULL listening path (req/s). Prototypewebjs startboth 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.serveand a cleaner embedded-host adapter.Follow-up to #508 (runtime support) and #509 (Bun test matrix).
Acceptance criteria
webjs starton Bun vianode:httpcompat vsBun.serveon the full listening path (req/s), recorded in the PR, justifies (or defers) the adapter.createRequestHandlerunchanged;startServerselects anode:httpadapter (Node) or aBun.serveadapter (Bun) at runtime.WSupgrade (toroute.tsWSexports), 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).bun --bun run start/devauto-selects the Bun adapter;bun test/bun/smoke.mjsand an e2e-style probe pass against the Bun-served app.agent-docs/deployment.md,packages/server/AGENTS.md(thedev.js/ startServer entry), and the scaffold's Bun-run note.