Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions src/adapters/BunWebSocketAdapter.ts
Original file line number Diff line number Diff line change
@@ -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<Response>;

export type BunBeforeFetchHandler = (
request: Request,
server: unknown,
next: () => Promise<Response>,
) => Response | undefined | Promise<Response | undefined>;

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?.();
},
};
}
}
1 change: 1 addition & 0 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./BunAdapter";
export * from "./BunWebSocketAdapter";
49 changes: 49 additions & 0 deletions tests/BunWebSocketAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>((resolve) => server.close(resolve));
}
});
});
Loading