Skip to content

Commit 4655fa6

Browse files
committed
Initial commit と༼ ◕_◕ と ༽
0 parents  commit 4655fa6

File tree

16 files changed

+986
-0
lines changed

16 files changed

+986
-0
lines changed

.github/copilot-instructions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Introduction
2+
3+
This library `bun-ws-router` is a WebSocket router for Bun implemented in TypeScript. It provides a simple and efficient way to handle WebSocket connections and route messages to different handlers based on the message type. It's intended to be used together with Zod-based validation for message types.

.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# code coverage
10+
coverage
11+
*.lcov
12+
13+
# logs
14+
logs
15+
_.log
16+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17+
18+
# dotenv environment variable files
19+
.env
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
.env.local
24+
25+
# caches
26+
.eslintcache
27+
.cache
28+
*.tsbuildinfo
29+
30+
# IntelliJ based IDEs
31+
.idea
32+
33+
# Finder (MacOS) folder config
34+
.DS_Store

.vscode/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"editor.defaultFormatter": "esbenp.prettier-vscode",
4+
"editor.codeActionsOnSave": {
5+
"source.organizeImports": "always"
6+
},
7+
"editor.tabSize": 2
8+
}

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025-present Konstantin Tarkus, Kriasoft (hello@kriasoft.com)
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Bun WebSocket Router
2+
3+
A type-safe WebSocket router for Bun with Zod-based message validation. Route WebSocket messages to handlers based on message type with full TypeScript support.
4+
5+
### Key Features
6+
7+
- **Type-safe messaging**: Built-in validation using Zod schemas
8+
- **Simple API**: Intuitive routing system for WebSocket messages
9+
- **Performance**: Leverages Bun's native WebSocket implementation
10+
- **Flexible**: Works with any Bun server setup, including Hono
11+
- **Room support**: Easily group clients and broadcast messages
12+
- **Lightweight**: Minimal dependencies for fast startup
13+
14+
Perfect for real-time applications like chat systems, live dashboards, multiplayer games, and notification services.
15+
16+
## Installation
17+
18+
```bash
19+
bun add bun-ws-router zod
20+
```
21+
22+
## Getting Started
23+
24+
The following example demonstrates how to set up a Bun server with both (RESTful) HTTP and WebSocket routers.
25+
26+
```ts
27+
import { Hono } from "hono";
28+
import { WebSocketRouter } from "bun-ws-router";
29+
import { exampleRouter } from "./example";
30+
31+
// HTTP router
32+
const app = new Hono();
33+
app.get("/", (c) => c.text("Welcome to Hono!"));
34+
35+
// WebSocket router
36+
const ws = new WebSocketRouter();
37+
ws.addRoutes(exampleRouter); // Add routes from another file
38+
39+
Bun.serve({
40+
port: 3000,
41+
42+
fetch(req, server) {
43+
const url = new URL(req.url);
44+
45+
// Handle WebSocket upgrade requests
46+
if (url.pathname === "/ws") {
47+
return ws.upgrade(req, {
48+
server,
49+
});
50+
}
51+
52+
// Handle regular HTTP requests
53+
return app.fetch(req, { server });
54+
},
55+
56+
// Handle WebSocket connections
57+
websocket: ws.websocket,
58+
});
59+
60+
console.log(`WebSocket server listening on ws://localhost:3000/ws`);
61+
```
62+
63+
## How to handle authentication
64+
65+
You can handle authentication by checking the `Authorization` header for a JWT token or any other authentication method you prefer. The following example demonstrates how to verify a JWT token and pass the user information to the WebSocket router.
66+
67+
```ts
68+
import { WebSocketRouter } from "bun-ws-router";
69+
import { DecodedIdToken } from "firebase-admin/auth";
70+
import { verifyIdToken } from "./auth"; // Your authentication logic
71+
72+
type Meta = {
73+
user?: DecodedIdToken | null;
74+
};
75+
76+
// WebSocket router
77+
const ws = new WebSocketRouter<Meta>();
78+
79+
Bun.serve({
80+
port: 3000,
81+
82+
async fetch(req, server) {
83+
const url = new URL(req.url);
84+
85+
// Check if the user is authenticated
86+
const user = await verifyToken(req);
87+
88+
// Handle WebSocket upgrade requests
89+
if (url.pathname === "/ws") {
90+
return ws.upgrade(req, {
91+
server,
92+
data: { user },
93+
});
94+
}
95+
96+
// Handle regular HTTP requests
97+
return await app.fetch(req, { server, user });
98+
},
99+
100+
// Handle WebSocket connections
101+
websocket: ws.websocket,
102+
});
103+
```
104+
105+
The `verifyIdToken` function is a placeholder for your authentication logic which could use user ID token verification from `firebase-admin` or any other authentication library.
106+
107+
## How to define message types
108+
109+
You can define message types using the `messageSchema` function from `bun-ws-router`. This function takes a message type name such as `JOIN_ROOM`, `SEND_MESSAGE` etc. and a Zod schema for the message payload. The following example demonstrates how to define message types for a chat application.
110+
111+
```ts
112+
import { messageSchema } from "bun-ws-router";
113+
import { z } from "zod";
114+
115+
export const JoinRoom = messageSchema("JOIN_ROOM", {
116+
roomId: z.string(),
117+
});
118+
119+
export const UserJoined = messageSchema("USER_JOINED", {
120+
roomId: z.string(),
121+
userId: z.string(),
122+
});
123+
124+
export const UserLeft = messageSchema("USER_LEFT", {
125+
userId: z.string(),
126+
});
127+
128+
export const SendMessage = messageSchema("SEND_MESSAGE", {
129+
roomId: z.string(),
130+
message: z.string(),
131+
});
132+
```
133+
134+
## How to define routes
135+
136+
You can define routes using the `WebSocketRouter` instance methods: `onOpen`, `onMessage`, and `onClose`.
137+
138+
```ts
139+
import { WebSocketRouter } from "bun-ws-router";
140+
import { Meta, JoinRoom, UserJoined, SendMessage, UserLeft } from "./schema";
141+
142+
const ws = new WebSocketRouter<Meta>();
143+
144+
// Handle new connections
145+
ws.onOpen((c) => {
146+
console.log(
147+
`Client connected: ${c.ws.data.clientId}, User ID: ${c.ws.data.userId}`
148+
);
149+
// You could send a welcome message here
150+
});
151+
152+
// Handle specific message types
153+
ws.onMessage(JoinRoom, (c) => {
154+
const { roomId } = c.payload;
155+
const userId = c.ws.data.userId || c.ws.data.clientId; // Use userId if available, else clientId
156+
c.ws.data.roomId = roomId; // Store room in connection data
157+
console.log(`User ${userId} joining room: ${roomId}`);
158+
159+
// Example: Send confirmation back or broadcast to room
160+
// This requires implementing broadcast/room logic separately
161+
// c.send(UserJoined, { roomId, userId });
162+
});
163+
164+
ws.onMessage(SendMessage, (c) => {
165+
const { message } = c.payload;
166+
const userId = c.ws.data.userId || c.ws.data.clientId;
167+
const roomId = c.ws.data.roomId;
168+
console.log(`Message in room ${roomId} from ${userId}: ${message}`);
169+
// Add logic to broadcast message to others in the room
170+
});
171+
172+
// Handle disconnections
173+
ws.onClose((c) => {
174+
const userId = c.ws.data.userId || c.ws.data.clientId;
175+
console.log(`Client disconnected: ${userId}, code: ${c.code}`);
176+
// Example: Notify others in the room the user left
177+
// This requires implementing broadcast/room logic separately
178+
// broadcast(c.ws.data.roomId, UserLeft, { userId });
179+
});
180+
```
181+
182+
**Note:** The `c.send(...)` function sends a message back to the _current_ client.
183+
184+
## How to compose routes
185+
186+
You can compose routes from different files into a single router. This is useful for organizing your code and keeping related routes together.
187+
188+
```ts
189+
import { WebSocketRouter } from "bun-ws-router";
190+
import { Meta } from "./schemas";
191+
import { chatRoutes } from "./chat";
192+
import { notificationRoutes } from "./notification";
193+
194+
const ws = new WebSocketRouter<Meta>();
195+
ws.addRoutes(chatRoutes);
196+
ws.addRoutes(notificationRoutes);
197+
```
198+
199+
Where `chatRoutes` and `notificationRoutes` are other router instances defined in separate files.
200+
201+
## Support
202+
203+
Feel free to discuss any issues or suggestions on our [Discord](https://discord.com/invite/bSsv7XM) channel. We welcome contributions and feedback from the community.
204+
205+
## Backers
206+
207+
<a href="https://reactstarter.com/b/1"><img src="https://reactstarter.com/b/1.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/2"><img src="https://reactstarter.com/b/2.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/3"><img src="https://reactstarter.com/b/3.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/4"><img src="https://reactstarter.com/b/4.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/5"><img src="https://reactstarter.com/b/5.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/6"><img src="https://reactstarter.com/b/6.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/7"><img src="https://reactstarter.com/b/7.png" height="60" /></a>&nbsp;&nbsp;<a href="https://reactstarter.com/b/8"><img src="https://reactstarter.com/b/8.png" height="60" /></a>
208+
209+
## License
210+
211+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

bun.lock

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"lockfileVersion": 1,
3+
"workspaces": {
4+
"": {
5+
"name": "bun-ws-router",
6+
"dependencies": {
7+
"uuid": "^11.1.0",
8+
},
9+
"devDependencies": {
10+
"@types/bun": "^1.2.10",
11+
"hono": "^4.7.7",
12+
"typescript": "^5.8.3",
13+
"zod": "^3.24.3",
14+
},
15+
"peerDependencies": {
16+
"@types/bun": ">=1.2.0",
17+
"zod": ">=3.0.0",
18+
},
19+
},
20+
},
21+
"packages": {
22+
"@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="],
23+
24+
"@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="],
25+
26+
"bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="],
27+
28+
"hono": ["hono@4.7.7", "", {}, "sha512-2PCpQRbN87Crty8/L/7akZN3UyZIAopSoRxCwRbJgUuV1+MHNFHzYFxZTg4v/03cXUm+jce/qa2VSBZpKBm3Qw=="],
29+
30+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
31+
32+
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
33+
34+
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
35+
36+
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
37+
}
38+
}

example/example.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { WebSocketRouter } from "../router";
2+
import { JoinRoomSchema, UserJoinedSchema } from "./schema";
3+
4+
const ws = new WebSocketRouter();
5+
6+
ws.onMessage(JoinRoomSchema, (c) => {
7+
const { roomId } = c.payload;
8+
console.log(`User joined room: ${roomId}`);
9+
10+
c.send(UserJoinedSchema, {
11+
roomId,
12+
userId: c.meta.clientId,
13+
});
14+
});
15+
16+
ws.onClose((c) => {
17+
console.log(`Connection closed`);
18+
});
19+
20+
export { ws as exampleRouter };

example/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Hono } from "hono";
2+
import { WebSocketRouter } from "../index";
3+
import { exampleRouter } from "./example";
4+
5+
// HTTP router
6+
const app = new Hono();
7+
app.get("/", (c) => c.text("Welcome to Hono!"));
8+
9+
// WebSocket router
10+
const ws = new WebSocketRouter();
11+
ws.addRoutes(exampleRouter);
12+
13+
Bun.serve({
14+
port: 3000,
15+
16+
fetch(req, server) {
17+
const url = new URL(req.url);
18+
19+
// Handle WebSocket upgrade requests
20+
if (url.pathname === "/ws") {
21+
return ws.upgrade(req, { server });
22+
}
23+
24+
// Handle regular HTTP requests
25+
return app.fetch(req, { server });
26+
},
27+
28+
// Handle WebSocket connections
29+
websocket: ws.websocket,
30+
});

example/schema.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { z } from "zod";
2+
import { MessageSchema } from "../schema";
3+
4+
export const JoinRoomSchema = MessageSchema.extend({
5+
type: z.literal("JOIN_ROOM"),
6+
payload: z.object({
7+
roomId: z.string(),
8+
}),
9+
});
10+
11+
export const UserJoinedSchema = MessageSchema.extend({
12+
type: z.literal("USER_JOINED"),
13+
payload: z.object({
14+
roomId: z.string(),
15+
userId: z.string().optional(),
16+
}),
17+
});

handlers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* SPDX-FileCopyrightText: 2025-present Kriasoft */
2+
/* SPDX-License-Identifier: MIT */
3+
4+
import type { CloseHandler, MessageHandlerEntry, OpenHandler } from "./types";
5+
6+
export class WebSocketHandlers<T = any> {
7+
public readonly open: OpenHandler<T>[] = [];
8+
public readonly close: CloseHandler<T>[] = [];
9+
public readonly message = new Map<string, MessageHandlerEntry>();
10+
}

0 commit comments

Comments
 (0)