alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
136 lines (115 loc) • 3.84 kB
text/typescript
import { $env, $hook, $inject, Alepha, type Static, z } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import { $logger } from "alepha/logger";
import { ServerProvider } from "./ServerProvider.ts";
import { ServerRouterProvider } from "./ServerRouterProvider.ts";
const envSchema = z.object({
SERVER_PORT: z
.integer()
.meta({ min: 0, max: 65535 })
.describe("Set 0 to listen on a random port.")
.default(3000),
SERVER_HOST: z.text({
default: "localhost",
description: "Set 0.0.0.0 to listen on all interfaces.",
}),
});
declare module "alepha" {
interface Env extends Partial<Static<typeof envSchema>> {}
}
export class BunHttpServerProvider extends ServerProvider {
protected readonly alepha = $inject(Alepha);
protected readonly dateTimeProvider = $inject(DateTimeProvider);
protected readonly log = $logger();
protected readonly env = $env(envSchema);
protected readonly router = $inject(ServerRouterProvider);
protected bunServer?: ReturnType<typeof Bun.serve>;
public get hostname(): string {
if (this.bunServer) {
return `http://${this.bunServer.hostname}:${this.bunServer.port}`;
}
return `http://${this.env.SERVER_HOST}:${this.env.SERVER_PORT}`;
}
public readonly start = $hook({
on: "start",
handler: async () => {
await this.listen();
},
});
protected readonly stop = $hook({
on: "stop",
handler: async () => {
if (this.alepha.isProduction()) {
await this.close();
return;
}
// do not await in development & test
this.close().catch(() => {});
},
});
protected async listen() {
let port = this.env.SERVER_PORT;
// for testing, use a random port if port is 3000 (default)
if (this.alepha.isTest() && port === 3000) {
port = 0;
}
try {
this.bunServer = Bun.serve({
port,
hostname: this.env.SERVER_HOST,
fetch: async (request: Request) => {
this.log.trace(`Incoming Bun request -> ${request.url}`);
// Create WebRequestEvent for handleWebRequest
const webRequestEvent = {
req: request,
res: undefined as Response | undefined,
};
try {
await this.handleWebRequest(webRequestEvent);
if (!webRequestEvent.res) {
// No response set, return 500
return new Response("Internal Server Error", {
status: 500,
headers: { "content-type": "text/plain" },
});
}
return webRequestEvent.res;
} catch (err) {
this.log.error("Error handling request", err);
return new Response("Internal Server Error", {
status: 500,
headers: { "content-type": "text/plain" },
});
}
},
error: (error: Error) => {
this.log.error("Bun server error", error);
return new Response("Internal Server Error", {
status: 500,
headers: { "content-type": "text/plain" },
});
},
});
this.log.info(`Server listening on ${this.hostname}/`);
} catch (err) {
this.log.error("Failed to start Bun server", err);
throw err;
}
}
protected async close() {
if (!this.bunServer) {
return;
}
try {
// Bun's server.stop() returns a promise that resolves when all connections are closed
const stopPromise = this.bunServer.stop();
// Race between stop completion and timeout
await Promise.race([this.dateTimeProvider.wait(10000), stopPromise]);
this.bunServer = undefined;
this.log.info("Server closed");
} catch (err) {
this.log.error("Error closing Bun server", err);
throw err;
}
}
}