Skip to content

Commit

Permalink
feat: support Bun's http server
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed Mar 5, 2024
1 parent 46e9e07 commit 7f08edc
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 9 deletions.
130 changes: 130 additions & 0 deletions http_server_bun.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2018-2024 the oak authors. All rights reserved. MIT license.

// deno-lint-ignore-file no-explicit-any

import { assert, assertEquals } from "./deps_test.ts";

import Server from "./http_server_bun.ts";

interface SocketAddress {
address: string;
port: number;
family: "IPv4" | "IPv6";
}

let currentServer: MockBunServer | undefined;
let requests: Request[] = [];

class MockBunServer {
stoppedCount = 0;
fetch: (
req: Request,
server: this,
) => Response | Promise<Response>;
responses: Response[] = [];
runPromise: Promise<void>;

development: boolean;
hostname: string;
port: number;
pendingRequests = 0;

async #run() {
for (const req of requests) {
const res = await this.fetch(req, this);
this.responses.push(res);
}
}

constructor(
{ fetch, hostname, port, development }: {
fetch: (
req: Request,
server: unknown,
) => Response | Promise<Response>;
hostname?: string;
port?: number;
development?: boolean;
error?: (error: Error) => Response | Promise<Response>;
tls?: {
key?: string;
cert?: string;
};
},
) {
this.fetch = fetch;
this.development = development ?? false;
this.hostname = hostname ?? "localhost";
this.port = port ?? 567890;
currentServer = this;
this.runPromise = this.#run();
}

requestIP(_req: Request): SocketAddress | null {
return { address: "127.0.0.0", port: 567890, family: "IPv4" };
}

stop(): void {
this.stoppedCount++;
}
}

function setup(reqs?: Request[]) {
if (reqs) {
requests = reqs;
}
(globalThis as any)["Bun"] = {
serve(options: any) {
return new MockBunServer(options);
},
};
}

function teardown() {
delete (globalThis as any)["Bun"];
currentServer = undefined;
}

Deno.test({
name: "bun server can listen",
async fn() {
setup();
const server = new Server({ port: 8080 });
const listener = await server.listen();
assertEquals(listener, {
addr: { hostname: "localhost", port: 8080, transport: "tcp" },
});
assert(currentServer);
assertEquals(currentServer.stoppedCount, 0);
await server.close();
assertEquals(currentServer.stoppedCount, 1);
teardown();
},
});

Deno.test({
name: "bun server can process requests",
async fn() {
setup([new Request(new URL("http://localhost:8080/"))]);
const server = new Server({ port: 8080 });
const listener = await server.listen();
assertEquals(listener, {
addr: { hostname: "localhost", port: 8080, transport: "tcp" },
});
assert(currentServer);
// deno-lint-ignore no-async-promise-executor
const promise = new Promise<void>(async (resolve) => {
for await (const req of server) {
assert(req.request);
assertEquals(req.request.url, "http://localhost:8080/");
req.respond(new Response("hello world"));
}
resolve();
});
await server.close();
await promise;
assertEquals(currentServer.stoppedCount, 1);
assertEquals(currentServer.responses.length, 1);
teardown();
},
});
197 changes: 197 additions & 0 deletions http_server_bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright 2022-2024 the oak authors. All rights reserved.

/**
* The implementation of the acorn server interface for Deno CLI and Deno
* Deploy.
*
* @module
*/

import type {
Addr,
Listener,
RequestEvent as _RequestEvent,
ServeOptions,
Server,
ServeTlsOptions,
} from "./types_internal.ts";
import { createPromiseWithResolvers } from "./util.ts";

type TypedArray =
| Uint8Array
| Uint16Array
| Uint32Array
| Int8Array
| Int16Array
| Int32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array
| Uint8ClampedArray;
type BunFile = File;

interface Bun {
serve(options: {
fetch: (req: Request, server: BunServer) => Response | Promise<Response>;
hostname?: string;
port?: number;
development?: boolean;
error?: (error: Error) => Response | Promise<Response>;
tls?: {
key?:
| string
| TypedArray
| BunFile
| Array<string | TypedArray | BunFile>;
cert?:
| string
| TypedArray
| BunFile
| Array<string | TypedArray | BunFile>;
ca?: string | TypedArray | BunFile | Array<string | TypedArray | BunFile>;
passphrase?: string;
dhParamsFile?: string;
};
maxRequestBodySize?: number;
lowMemoryMode?: boolean;
}): BunServer;
}

interface BunServer {
development: boolean;
hostname: string;
port: number;
pendingRequests: number;
requestIP(req: Request): SocketAddress | null;
stop(): void;
upgrade(req: Request, options?: {
headers?: HeadersInit;
//deno-lint-ignore no-explicit-any
data?: any;
}): boolean;
}

interface SocketAddress {
address: string;
port: number;
family: "IPv4" | "IPv6";
}

declare const Bun: Bun;

function isServeTlsOptions(
value: Omit<ServeOptions | ServeTlsOptions, "signal">,
): value is Omit<ServeTlsOptions, "signal"> {
return !!("cert" in value && "key" in value);
}

class RequestEvent implements _RequestEvent {
#promise: Promise<Response>;
// deno-lint-ignore no-explicit-any
#reject: (reason?: any) => void;
#request: Request;
#resolve: (value: Response | PromiseLike<Response>) => void;
#resolved = false;
#socketAddr: SocketAddress | null;

get addr(): Addr {
return {
transport: "tcp",
hostname: this.#socketAddr?.address ?? "",
port: this.#socketAddr?.port ?? 0,
};
}

get request(): Request {
return this.#request;
}

get response(): Promise<Response> {
return this.#promise;
}

constructor(request: Request, server: BunServer) {
this.#request = request;
this.#socketAddr = server.requestIP(request);
const { resolve, reject, promise } = createPromiseWithResolvers<Response>();
this.#resolve = resolve;
this.#reject = reject;
this.#promise = promise;
}

// deno-lint-ignore no-explicit-any
error(reason?: any): void {
if (this.#resolved) {
throw new Error("Request already responded to.");
}
this.#resolved = true;
this.#reject(reason);
}

respond(response: Response | PromiseLike<Response>): void {
if (this.#resolved) {
throw new Error("Request already responded to.");
}
this.#resolved = true;
this.#resolve(response);
}
}

export default class Steamer implements Server {
#controller?: ReadableStreamDefaultController<RequestEvent>;
#options: Omit<ServeOptions | ServeTlsOptions, "signal">;
#server?: BunServer;
#stream?: ReadableStream<RequestEvent>;

constructor(options: Omit<ServeOptions | ServeTlsOptions, "signal">) {
this.#options = options;
}

close(): void | Promise<void> {
this.#controller?.close();
this.#server?.stop();
this.#server = undefined;
}

listen(): Listener | Promise<Listener> {
if (this.#server) {
throw new Error("Server already listening.");
}
const { onListen, hostname, port } = this.#options;
const tls = isServeTlsOptions(this.#options)
? { key: this.#options.key, cert: this.#options.cert }
: undefined;
const { promise, resolve } = createPromiseWithResolvers<Listener>();
this.#stream = new ReadableStream<RequestEvent>({
start: (controller) => {
this.#controller = controller;
this.#server = Bun.serve({
fetch(req, server) {
const request = new RequestEvent(req, server);
controller.enqueue(request);
return request.response;
},
hostname,
port,
tls,
});
{
const { hostname, port } = this.#server;
if (onListen) {
onListen({ hostname, port });
}
resolve({ addr: { hostname, port, transport: "tcp" } });
}
},
});
return promise;
}

[Symbol.asyncIterator](): AsyncIterableIterator<RequestEvent> {
if (!this.#stream) {
throw new TypeError("Server hasn't started listening.");
}
return this.#stream[Symbol.asyncIterator]();
}
}
4 changes: 2 additions & 2 deletions http_server_deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class RequestEvent implements _RequestEvent {
//deno-lint-ignore no-explicit-any
#reject: (reason?: any) => void;
#request: Request;
#resolve: (value: Response) => void;
#resolve: (value: Response | PromiseLike<Response>) => void;
#resolved = false;
#response: Promise<Response>;

Expand Down Expand Up @@ -87,7 +87,7 @@ class RequestEvent implements _RequestEvent {
this.#reject(reason);
}

respond(response: Response): void {
respond(response: Response | PromiseLike<Response>): void {
if (this.#resolved) {
throw new Error("Request already responded to.");
}
Expand Down
15 changes: 9 additions & 6 deletions router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {
SecureCookieMap,
Status,
} from "./deps.ts";
import Server from "./http_server_deno.ts";
import type { Deserializer, KeyRing, Serializer } from "./types.ts";
import type {
Addr,
Expand All @@ -54,6 +53,7 @@ import {
CONTENT_TYPE_TEXT,
createPromiseWithResolvers,
isBodyInit,
isBun,
isHtmlLike,
isJsonLike,
responseFromHttpError,
Expand Down Expand Up @@ -748,6 +748,8 @@ export interface RouterHandleInit {
secure?: boolean;
}

let ServerCtor: ServerConstructor | undefined;

/** A router which is specifically geared for handling RESTful type of requests
* and providing a straight forward API to respond to them.
*
Expand Down Expand Up @@ -900,10 +902,8 @@ export class Router extends EventTarget {
performance.mark(`${HANDLE_START} ${uid}`);
const { promise, resolve } = Promise.withResolvers<Response>();
this.#handling.add(promise);
promise.then((response) => {
requestEvent.respond(response);
this.#handling.delete(promise);
}).catch(
requestEvent.respond(promise);
promise.then(() => this.#handling.delete(promise)).catch(
(error) => {
this.#error(requestEvent.request, error, false);
},
Expand Down Expand Up @@ -1463,7 +1463,10 @@ export class Router extends EventTarget {
async listen(options: ListenOptions = { port: 0 }): Promise<void> {
const {
secure = false,
server: _Server = Server,
server: _Server = ServerCtor ??
(ServerCtor = isBun()
? (await import("./http_server_bun.ts")).default
: (await import("./http_server_deno.ts")).default),
signal,
...listenOptions
} = options;
Expand Down
Loading

0 comments on commit 7f08edc

Please sign in to comment.