diff --git a/README.md b/README.md index 1f3dc71..a67d729 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,30 @@ const fileUpload = new FileUploadModule({ }); ``` +## WebSocket integrations + +Use `BunWebSocketAdapter` when a WebSocket library needs direct access to Bun's native `fetch` and `websocket` handlers while keeping the normal Bun Platform Kit router as the fallback. + +```typescript +import { BunKitServer, BunWebSocketAdapter } from "bun-platform-kit"; + +const adapter = new BunWebSocketAdapter({ + beforeFetch: (request, server, next) => { + const url = new URL(request.url); + if (!url.pathname.startsWith("/socket.io/")) { + return next(); + } + + return engine.handleRequest(request, server); + }, + websocket: engine.handler().websocket, +}); + +const server = new BunKitServer(3000, { adapter }); +``` + +The adapter does not depend on Socket.IO; consumers can plug in any Bun-compatible WebSocket engine. + ## Contributing 1. Fork the repository diff --git a/src/adapters/BunWebSocketAdapter.ts b/src/adapters/BunWebSocketAdapter.ts new file mode 100644 index 0000000..366a29c --- /dev/null +++ b/src/adapters/BunWebSocketAdapter.ts @@ -0,0 +1,64 @@ +import type { ServerApp, ServerInstance } from "../abstract/ServerTypes"; +import { BunAdapter } from "./BunAdapter"; + +declare const Bun: any; + +type BunFetchHandler = ( + request: Request, + server?: unknown, +) => Response | Promise; + +export type BunBeforeFetchHandler = ( + request: Request, + server: unknown, + next: () => Promise, +) => Response | undefined | Promise; + +export type BunWebSocketAdapterOptions = { + beforeFetch: BunBeforeFetchHandler; + websocket: unknown; +}; + +export class BunWebSocketAdapter extends BunAdapter { + constructor(private readonly options: BunWebSocketAdapterOptions) { + super(); + } + + override listen( + app: ServerApp, + port: number, + onListen: () => void, + ): ServerInstance { + const bunApp = app as ServerApp & { + get?(key: string): unknown; + createFetchHandler?: () => BunFetchHandler; + }; + + if (typeof bunApp.createFetchHandler !== "function") { + throw new Error( + "BunWebSocketAdapter requires the Bun runtime application", + ); + } + + const appFetch = bunApp.createFetchHandler(); + const hostname = bunApp.get?.("hostname") as string | undefined; + const server = Bun.serve({ + port, + hostname, + fetch: (request: Request, bunServer: unknown) => + this.options.beforeFetch(request, bunServer, () => + Promise.resolve(appFetch(request, bunServer)), + ), + websocket: this.options.websocket, + }); + + onListen(); + + return { + close: (callback?: () => void) => { + server.stop(true); + callback?.(); + }, + }; + } +} diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 6beaaf6..6f37d62 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -1 +1,2 @@ export * from "./BunAdapter"; +export * from "./BunWebSocketAdapter"; diff --git a/tests/BunWebSocketAdapter.test.ts b/tests/BunWebSocketAdapter.test.ts new file mode 100644 index 0000000..9c3ab44 --- /dev/null +++ b/tests/BunWebSocketAdapter.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "bun:test"; +import { BunWebSocketAdapter } from "../src/adapters"; + +describe("BunWebSocketAdapter", () => { + it("runs raw fetch handling before the normal Bun router", async () => { + const port = 35_000 + Math.floor(Math.random() * 20_000); + let receivedServer = false; + let delegatedToRouter = false; + + const adapter = new BunWebSocketAdapter({ + beforeFetch: (request, server, next) => { + receivedServer = server !== undefined; + const url = new URL(request.url); + if (url.pathname === "/socket.io/") { + return new Response("socket-handler"); + } + + delegatedToRouter = true; + return next(); + }, + websocket: { + message() {}, + }, + }); + + const app = adapter.createApp(); + app.set?.("hostname", "127.0.0.1"); + adapter.configure(app, port); + + const server = adapter.listen(app, port, () => undefined); + + try { + const socketResponse = await fetch( + `http://127.0.0.1:${port}/socket.io/?EIO=4&transport=polling`, + ); + expect(socketResponse.status).toBe(200); + expect(await socketResponse.text()).toBe("socket-handler"); + expect(receivedServer).toBe(true); + + const fallbackResponse = await fetch( + `http://127.0.0.1:${port}/not-registered`, + ); + expect(delegatedToRouter).toBe(true); + expect(fallbackResponse.status).toBe(404); + } finally { + await new Promise((resolve) => server.close(resolve)); + } + }); +});