first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* node:http adapter — bridge a WinterCG `fetch(request) => Response` handler
|
||||
* onto a Node HTTP server, with no external dependencies. Converts a Node
|
||||
* `IncomingMessage` into a web `Request` and writes a web `Response` back into a
|
||||
* `ServerResponse` (preserving multiple `Set-Cookie` headers).
|
||||
*
|
||||
* Caveat: the production handler uses Bun-native APIs (Bun.file for assets,
|
||||
* Bun.serve for websockets, Bun.SQL / bun:sqlite for the database), so running
|
||||
* the FULL app under plain Node needs Bun-compatible globals. This adapter is
|
||||
* for WinterCG hosts and for embedding the handler behind an existing
|
||||
* `node:http` server; the Request/Response conversion itself is fully portable.
|
||||
*/
|
||||
|
||||
import type { IncomingMessage, ServerResponse, Server } from "node:http";
|
||||
|
||||
export type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;
|
||||
|
||||
/** Convert a Node IncomingMessage into a web Request (buffers the body). */
|
||||
export async function toRequest(
|
||||
req: IncomingMessage,
|
||||
opts: { origin?: string } = {},
|
||||
): Promise<Request> {
|
||||
const method = req.method ?? "GET";
|
||||
const host = req.headers.host ?? "localhost";
|
||||
const proto = (asString(req.headers["x-forwarded-proto"]) ?? "http").split(",")[0]!.trim();
|
||||
const origin = opts.origin ?? `${proto}://${host}`;
|
||||
const url = new URL(req.url ?? "/", origin);
|
||||
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (value === undefined) continue;
|
||||
if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
||||
else headers.set(key, value);
|
||||
}
|
||||
|
||||
const hasBody = method !== "GET" && method !== "HEAD";
|
||||
const body = hasBody ? ((await readBody(req)) as BodyInit) : undefined;
|
||||
return new Request(url, { method, headers, body });
|
||||
}
|
||||
|
||||
function asString(value: string | string[] | undefined): string | undefined {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
function readBody(req: IncomingMessage): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on("data", (c: Buffer) => chunks.push(c));
|
||||
req.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** Write a web Response into a Node ServerResponse. */
|
||||
export async function writeResponse(res: ServerResponse, response: Response): Promise<void> {
|
||||
const headers: Record<string, string | string[]> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
if (key.toLowerCase() !== "set-cookie") headers[key] = value;
|
||||
});
|
||||
// Multiple Set-Cookie headers must stay separate (Headers.forEach joins them).
|
||||
const getSetCookie = (response.headers as { getSetCookie?: () => string[] }).getSetCookie;
|
||||
const cookies = typeof getSetCookie === "function" ? getSetCookie.call(response.headers) : [];
|
||||
if (cookies.length) headers["set-cookie"] = cookies;
|
||||
|
||||
res.writeHead(response.status, headers);
|
||||
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
res.write(value);
|
||||
}
|
||||
} else {
|
||||
const buf = new Uint8Array(await response.arrayBuffer());
|
||||
if (buf.length) res.write(buf);
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
|
||||
/** A `node:http` request listener that dispatches to a fetch handler. */
|
||||
export function nodeListener(handler: FetchHandler, opts: { origin?: string } = {}) {
|
||||
return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
||||
try {
|
||||
const response = await handler(await toRequest(req, opts));
|
||||
if (!response) {
|
||||
// A missing response means the handler expected a protocol upgrade
|
||||
// (e.g. a WebSocket), which this HTTP adapter does not perform.
|
||||
res.writeHead(426, { "content-type": "text/plain" });
|
||||
res.end("Upgrade Required");
|
||||
return;
|
||||
}
|
||||
await writeResponse(res, response);
|
||||
} catch (err) {
|
||||
if (!res.headersSent) res.writeHead(500, { "content-type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
console.error("[wrnexus] node adapter error:", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Create and start a `node:http` server for a fetch handler. */
|
||||
export async function serveNode(
|
||||
handler: FetchHandler,
|
||||
opts: { port?: number; hostname?: string } = {},
|
||||
): Promise<Server> {
|
||||
const { createServer } = await import("node:http");
|
||||
const server = createServer(nodeListener(handler, {}));
|
||||
const port = opts.port ?? 3000;
|
||||
server.listen(port, opts.hostname ?? "0.0.0.0");
|
||||
console.log(`WrNexus (node adapter) listening on http://localhost:${port}`);
|
||||
return server;
|
||||
}
|
||||
Reference in New Issue
Block a user