bxo 0.0.5-dev.75 → 0.0.5-dev.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,7 @@ A fast, lightweight web framework for Bun with built-in Zod validation and lifec
5
5
  ## Features
6
6
 
7
7
  - **Type-safe routing** with Zod schema validation
8
+ - **WebSocket support** with clean API
8
9
  - **Lifecycle hooks** for middleware and plugins
9
10
  - **Plugin system** for extending functionality
10
11
  - **Built-in CORS support** via plugin
@@ -33,6 +34,76 @@ app.get("/", (ctx) => ctx.json({ message: "Hello World!" }));
33
34
  app.start();
34
35
  ```
35
36
 
37
+ ## WebSocket Support
38
+
39
+ BXO provides built-in WebSocket support with a clean, intuitive API:
40
+
41
+ ```typescript
42
+ import BXO from "./src";
43
+
44
+ const app = new BXO();
45
+
46
+ // WebSocket route
47
+ app.ws("/ws", {
48
+ open(ws) {
49
+ console.log("WebSocket connection opened");
50
+ ws.send("Welcome to BXO WebSocket!");
51
+ },
52
+
53
+ message(ws, message) {
54
+ console.log("Received message:", message);
55
+ // Echo the message back
56
+ ws.send(`Echo: ${message}`);
57
+ },
58
+
59
+ close(ws, code, reason) {
60
+ console.log(`WebSocket connection closed: ${code} ${reason}`);
61
+ },
62
+
63
+ ping(ws, data) {
64
+ console.log("Ping received:", data);
65
+ },
66
+
67
+ pong(ws, data) {
68
+ console.log("Pong received:", data);
69
+ }
70
+ });
71
+
72
+ // WebSocket with path parameters
73
+ app.ws("/chat/:room", {
74
+ open(ws) {
75
+ const room = ws.data?.room || 'unknown';
76
+ console.log(`WebSocket connection opened for room: ${room}`);
77
+ ws.send(`Welcome to chat room: ${room}`);
78
+ },
79
+
80
+ message(ws, message) {
81
+ const room = ws.data?.room || 'unknown';
82
+ console.log(`Message in room ${room}:`, message);
83
+ ws.send(`[${room}] Echo: ${message}`);
84
+ }
85
+ });
86
+
87
+ app.start();
88
+ ```
89
+
90
+ ### WebSocket Handler Events
91
+
92
+ - `open(ws)` - Called when a WebSocket connection is established
93
+ - `message(ws, message)` - Called when a message is received
94
+ - `close(ws, code, reason)` - Called when the connection is closed
95
+ - `drain(ws)` - Called when the WebSocket is ready for more data
96
+ - `ping(ws, data)` - Called when a ping is received
97
+ - `pong(ws, data)` - Called when a pong is received
98
+
99
+ ### WebSocket Features
100
+
101
+ - **Path parameters** - Support for dynamic routes like `/chat/:room`
102
+ - **Automatic upgrade** - HTTP requests to WebSocket routes are automatically upgraded
103
+ - **Type safety** - Full TypeScript support with proper typing
104
+ - **Error handling** - Built-in error handling for WebSocket events
105
+ - **Data attachment** - Access to path information via `ws.data`
106
+
36
107
  ## Lifecycle Hooks
37
108
 
38
109
  BXO provides powerful lifecycle hooks that allow you to intercept and modify requests and responses at different stages:
@@ -220,6 +291,7 @@ Check out the `example/` directory for more usage examples:
220
291
 
221
292
  - `cors-example.ts` - Demonstrates CORS plugin and lifecycle hooks
222
293
  - `openapi-example.ts` - Demonstrates OpenAPI plugin with tags and security
294
+ - `websocket-example.ts` - Demonstrates WebSocket functionality with interactive HTML client
223
295
  - `index.ts` - Basic routing example
224
296
 
225
297
  This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,132 @@
1
+ import BXO from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3000 } });
5
+
6
+ // HTTP routes
7
+ app.get("/", (ctx) => {
8
+ return ctx.text(`
9
+ <!DOCTYPE html>
10
+ <html>
11
+ <head>
12
+ <title>BXO WebSocket Example</title>
13
+ </head>
14
+ <body>
15
+ <h1>BXO WebSocket Example</h1>
16
+ <div id="messages"></div>
17
+ <input type="text" id="messageInput" placeholder="Type a message...">
18
+ <button onclick="sendMessage()">Send</button>
19
+ <button onclick="connect()">Connect</button>
20
+ <button onclick="disconnect()">Disconnect</button>
21
+
22
+ <script>
23
+ let ws = null;
24
+
25
+ function connect() {
26
+ ws = new WebSocket('ws://localhost:3000/ws');
27
+
28
+ ws.onopen = function() {
29
+ addMessage('Connected to WebSocket');
30
+ };
31
+
32
+ ws.onmessage = function(event) {
33
+ addMessage('Received: ' + event.data);
34
+ };
35
+
36
+ ws.onclose = function() {
37
+ addMessage('Disconnected from WebSocket');
38
+ };
39
+
40
+ ws.onerror = function(error) {
41
+ addMessage('Error: ' + error);
42
+ };
43
+ }
44
+
45
+ function disconnect() {
46
+ if (ws) {
47
+ ws.close();
48
+ ws = null;
49
+ }
50
+ }
51
+
52
+ function sendMessage() {
53
+ const input = document.getElementById('messageInput');
54
+ if (ws && input.value) {
55
+ ws.send(input.value);
56
+ addMessage('Sent: ' + input.value);
57
+ input.value = '';
58
+ }
59
+ }
60
+
61
+ function addMessage(message) {
62
+ const messages = document.getElementById('messages');
63
+ const div = document.createElement('div');
64
+ div.textContent = new Date().toLocaleTimeString() + ': ' + message;
65
+ messages.appendChild(div);
66
+ messages.scrollTop = messages.scrollHeight;
67
+ }
68
+
69
+ // Allow Enter key to send message
70
+ document.getElementById('messageInput').addEventListener('keypress', function(e) {
71
+ if (e.key === 'Enter') {
72
+ sendMessage();
73
+ }
74
+ });
75
+ </script>
76
+ `, 200, {
77
+ "Content-Type": "text/html"
78
+ });
79
+ });
80
+
81
+ // WebSocket route
82
+ app.ws("/ws", {
83
+ open(ws) {
84
+ console.log("WebSocket connection opened");
85
+ ws.send("Welcome to BXO WebSocket!");
86
+ },
87
+
88
+ message(ws, message) {
89
+ console.log("Received message:", message);
90
+ // Echo the message back
91
+ ws.send(`Echo: ${message}`);
92
+ },
93
+
94
+ close(ws, code, reason) {
95
+ console.log(`WebSocket connection closed: ${code} ${reason}`);
96
+ },
97
+
98
+ ping(ws, data) {
99
+ console.log("Ping received:", data);
100
+ },
101
+
102
+ pong(ws, data) {
103
+ console.log("Pong received:", data);
104
+ }
105
+ });
106
+
107
+ // Another WebSocket route with parameters
108
+ app.ws("/chat/:room", {
109
+ open(ws) {
110
+ console.log(`WebSocket connection opened for room: ${ws.data?.room || 'unknown'}`);
111
+ ws.send(`Welcome to chat room: ${ws.data?.room || 'unknown'}`);
112
+ },
113
+
114
+ message(ws, message) {
115
+ const room = ws.data?.room || 'unknown';
116
+ console.log(`Message in room ${room}:`, message);
117
+ ws.send(`[${room}] Echo: ${message}`);
118
+ },
119
+
120
+ close(ws, code, reason) {
121
+ const room = ws.data?.room || 'unknown';
122
+ console.log(`WebSocket connection closed for room ${room}: ${code} ${reason}`);
123
+ }
124
+ });
125
+
126
+ app.start();
127
+ console.log(`Server is running on http://localhost:${app.server?.port}`);
128
+ console.log(`WebSocket available at ws://localhost:${app.server?.port}/ws`);
129
+ console.log(`Chat WebSocket available at ws://localhost:${app.server?.port}/chat/:room`);
130
+ }
131
+
132
+ main().catch(console.error);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.5-dev.75",
8
+ "version": "0.0.5-dev.77",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
package/src/index.ts CHANGED
@@ -7,7 +7,8 @@ type Method =
7
7
  | "PATCH"
8
8
  | "DELETE"
9
9
  | "OPTIONS"
10
- | "HEAD";
10
+ | "HEAD"
11
+ | "WS";
11
12
 
12
13
  type PickWildcardName<S extends string> = S extends "" ? "wildcard" : S;
13
14
 
@@ -89,6 +90,16 @@ type Handler<P extends string, S extends RouteSchema | undefined = undefined> =
89
90
  app: BXO
90
91
  ) => Response | string | Promise<Response | string>;
91
92
 
93
+ // WebSocket handler types
94
+ export type WebSocketHandler<T = any> = {
95
+ message?(ws: Bun.ServerWebSocket<T>, message: string | Buffer): void | Promise<void>;
96
+ open?(ws: Bun.ServerWebSocket<T>): void | Promise<void>;
97
+ close?(ws: Bun.ServerWebSocket<T>, code: number, reason: string): void | Promise<void>;
98
+ drain?(ws: Bun.ServerWebSocket<T>): void | Promise<void>;
99
+ ping?(ws: Bun.ServerWebSocket<T>, data: Buffer): void | Promise<void>;
100
+ pong?(ws: Bun.ServerWebSocket<T>, data: Buffer): void | Promise<void>;
101
+ };
102
+
92
103
  type InternalRoute = {
93
104
  method: Method | "DEFAULT";
94
105
  path: string;
@@ -96,6 +107,7 @@ type InternalRoute = {
96
107
  paramNames: string[];
97
108
  schema?: RouteSchema;
98
109
  handler: AnyHandler;
110
+ websocketHandler?: WebSocketHandler;
99
111
  };
100
112
 
101
113
  type ServeOptions = Partial<Parameters<typeof Bun.serve>[0]>;
@@ -305,6 +317,10 @@ export default class BXO {
305
317
  return this.add("DELETE", path, handler as AnyHandler, schema as RouteSchema | undefined);
306
318
  }
307
319
 
320
+ ws<P extends string>(path: P, handler: WebSocketHandler): this {
321
+ return this.addWebSocket("WS", path, handler);
322
+ }
323
+
308
324
  // default can accept a handler OR static content (including Bun HTML bundle)
309
325
  default<P extends string>(path: P, handler: Handler<P, undefined>): this;
310
326
  default<P extends string, S extends RouteSchema>(path: P, handler: (req: Request) => Response, schema: S): this;
@@ -314,6 +330,9 @@ export default class BXO {
314
330
  }
315
331
 
316
332
  start(): void {
333
+ // Check if we have any WebSocket routes
334
+ const hasWebSocketRoutes = this.routes.some(r => r.method === "WS");
335
+
317
336
  // Build a basic routes map for Bun's native routes (exact paths only)
318
337
  const nativeRoutes: Record<string, Record<string, (req: Request) => Promise<Response> | Response>> = {};
319
338
 
@@ -322,6 +341,9 @@ export default class BXO {
322
341
  case "DEFAULT":
323
342
  nativeRoutes[r.path] = r.handler as any;
324
343
  break;
344
+ case "WS":
345
+ // Skip WebSocket routes in native routes - they'll be handled in websocket config
346
+ break;
325
347
  default:
326
348
  nativeRoutes[r.path] ||= {} as Record<string, (req: Request) => Promise<Response> | Response>;
327
349
  nativeRoutes[r.path][r.method] = (req: Request) => this.dispatch(r, req);
@@ -331,13 +353,61 @@ export default class BXO {
331
353
 
332
354
  this.serveOptions.port = this.serveOptions.port === undefined ? 3000 : this.serveOptions.port;
333
355
 
334
- this.server = Bun.serve({
335
- ...this.serveOptions,
336
- routes: nativeRoutes as any,
337
- fetch: (req: Request) => {
338
- return new Response("Not Found", { status: 404 });
356
+ // Create WebSocket configuration if we have WebSocket routes
357
+ const websocketConfig = hasWebSocketRoutes ? {
358
+ message: (ws: Bun.ServerWebSocket<{ path: string }>, message: string | Buffer) => {
359
+ this.handleWebSocketMessage(ws, message);
360
+ },
361
+ open: (ws: Bun.ServerWebSocket<{ path: string }>) => {
362
+ this.handleWebSocketOpen(ws);
363
+ },
364
+ close: (ws: Bun.ServerWebSocket<{ path: string }>, code: number, reason: string) => {
365
+ this.handleWebSocketClose(ws, code, reason);
366
+ },
367
+ drain: (ws: Bun.ServerWebSocket<{ path: string }>) => {
368
+ this.handleWebSocketDrain(ws);
369
+ },
370
+ ping: (ws: Bun.ServerWebSocket<{ path: string }>, data: Buffer) => {
371
+ this.handleWebSocketPing(ws, data);
372
+ },
373
+ pong: (ws: Bun.ServerWebSocket<{ path: string }>, data: Buffer) => {
374
+ this.handleWebSocketPong(ws, data);
339
375
  }
340
- });
376
+ } : undefined;
377
+
378
+ if (hasWebSocketRoutes) {
379
+ this.server = Bun.serve({
380
+ ...this.serveOptions,
381
+ routes: nativeRoutes as any,
382
+ websocket: websocketConfig as any,
383
+ fetch: (req: Request, server: Bun.Server) => {
384
+ // Handle WebSocket upgrade requests
385
+ if (req.headers.get("upgrade") === "websocket") {
386
+ const url = new URL(req.url);
387
+ const wsRoute = this.findWebSocketRoute(url.pathname);
388
+ if (wsRoute) {
389
+ const success = server.upgrade(req, {
390
+ data: { path: url.pathname }
391
+ });
392
+ if (success) {
393
+ return; // WebSocket upgrade successful
394
+ }
395
+ }
396
+ }
397
+
398
+ // Handle regular HTTP requests
399
+ return this.dispatchAny(req, nativeRoutes);
400
+ }
401
+ } as any);
402
+ } else {
403
+ this.server = Bun.serve({
404
+ ...this.serveOptions,
405
+ routes: nativeRoutes as any,
406
+ fetch: (req: Request) => {
407
+ return this.dispatchAny(req, nativeRoutes);
408
+ }
409
+ });
410
+ }
341
411
  }
342
412
 
343
413
  // Lifecycle hook methods
@@ -361,6 +431,93 @@ export default class BXO {
361
431
  return this;
362
432
  }
363
433
 
434
+ // WebSocket handler methods
435
+ private findWebSocketRoute(pathname: string): InternalRoute | null {
436
+ for (const route of this.routes) {
437
+ if (route.method === "WS") {
438
+ if (route.matcher === null) {
439
+ // Exact match
440
+ if (route.path === pathname) {
441
+ return route;
442
+ }
443
+ } else {
444
+ // Pattern match
445
+ const match = pathname.match(route.matcher);
446
+ if (match) {
447
+ return route;
448
+ }
449
+ }
450
+ }
451
+ }
452
+ return null;
453
+ }
454
+
455
+ private handleWebSocketMessage(ws: Bun.ServerWebSocket<{ path: string }>, message: string | Buffer): void {
456
+ const route = this.findWebSocketRoute(ws.data?.path || "");
457
+ if (route?.websocketHandler?.message) {
458
+ try {
459
+ (route.websocketHandler as any).message(ws, message);
460
+ } catch (error) {
461
+ console.error("WebSocket message handler error:", error);
462
+ }
463
+ }
464
+ }
465
+
466
+ private handleWebSocketOpen(ws: Bun.ServerWebSocket<{ path: string }>): void {
467
+ const route = this.findWebSocketRoute(ws.data?.path || "");
468
+ if (route?.websocketHandler?.open) {
469
+ try {
470
+ (route.websocketHandler as any).open(ws);
471
+ } catch (error) {
472
+ console.error("WebSocket open handler error:", error);
473
+ }
474
+ }
475
+ }
476
+
477
+ private handleWebSocketClose(ws: Bun.ServerWebSocket<{ path: string }>, code: number, reason: string): void {
478
+ const route = this.findWebSocketRoute(ws.data?.path || "");
479
+ if (route?.websocketHandler?.close) {
480
+ try {
481
+ (route.websocketHandler as any).close(ws, code, reason);
482
+ } catch (error) {
483
+ console.error("WebSocket close handler error:", error);
484
+ }
485
+ }
486
+ }
487
+
488
+ private handleWebSocketDrain(ws: Bun.ServerWebSocket<{ path: string }>): void {
489
+ const route = this.findWebSocketRoute(ws.data?.path || "");
490
+ if (route?.websocketHandler?.drain) {
491
+ try {
492
+ (route.websocketHandler as any).drain(ws);
493
+ } catch (error) {
494
+ console.error("WebSocket drain handler error:", error);
495
+ }
496
+ }
497
+ }
498
+
499
+ private handleWebSocketPing(ws: Bun.ServerWebSocket<{ path: string }>, data: Buffer): void {
500
+ const route = this.findWebSocketRoute(ws.data?.path || "");
501
+ if (route?.websocketHandler?.ping) {
502
+ try {
503
+ (route.websocketHandler as any).ping(ws, data);
504
+ } catch (error) {
505
+ console.error("WebSocket ping handler error:", error);
506
+ }
507
+ }
508
+ }
509
+
510
+ private handleWebSocketPong(ws: Bun.ServerWebSocket<{ path: string }>, data: Buffer): void {
511
+ const route = this.findWebSocketRoute(ws.data?.path || "");
512
+ if (route?.websocketHandler?.pong) {
513
+ try {
514
+ (route.websocketHandler as any).pong(ws, data);
515
+ } catch (error) {
516
+ console.error("WebSocket pong handler error:", error);
517
+ }
518
+ }
519
+ }
520
+
364
521
  // Internal
365
522
  private add(method: Method | "DEFAULT", path: string, handler: AnyHandler, schema?: RouteSchema): this {
366
523
  const { regex, names } = buildMatcher(path);
@@ -368,6 +525,12 @@ export default class BXO {
368
525
  return this;
369
526
  }
370
527
 
528
+ private addWebSocket(method: "WS", path: string, handler: WebSocketHandler): this {
529
+ const { regex, names } = buildMatcher(path);
530
+ this.routes.push({ method, path, handler: () => new Response("WebSocket route", { status: 400 }), matcher: regex, paramNames: names, websocketHandler: handler });
531
+ return this;
532
+ }
533
+
371
534
  private async dispatch(route: InternalRoute, req: Request): Promise<Response> {
372
535
  // Run beforeRequest hooks
373
536
  for (const hook of this.beforeRequestHooks) {
@@ -484,8 +647,10 @@ export default class BXO {
484
647
  return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
485
648
  }
486
649
  }
487
- const headers = mergeHeaders({ "Content-Type": "application/json" }, ctx.set.headers);
488
- return new Response(JSON.stringify(data), { status, headers });
650
+ return new Response(JSON.stringify(data), {
651
+ status,
652
+ headers: { "Content-Type": "application/json" }
653
+ });
489
654
  },
490
655
  text: (data, status = 200) => {
491
656
  if (route.schema?.response?.[status]) {
@@ -495,8 +660,10 @@ export default class BXO {
495
660
  return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
496
661
  }
497
662
  }
498
- const headers = mergeHeaders({ "Content-Type": "text/plain" }, ctx.set.headers);
499
- return new Response(String(data), { status, headers });
663
+ return new Response(String(data), {
664
+ status,
665
+ headers: { "Content-Type": "text/plain" }
666
+ });
500
667
  },
501
668
  status: (status, data) => {
502
669
  // Response validation if declared
@@ -508,14 +675,7 @@ export default class BXO {
508
675
  }
509
676
  }
510
677
 
511
- const resp = toResponse(data, { status });
512
- // Merge ctx.set.headers into final response
513
- const merged = new Response(resp.body, {
514
- status: resp.status,
515
- statusText: resp.statusText,
516
- headers: mergeHeaders(resp.headers, ctx.set.headers)
517
- });
518
- return merged;
678
+ return toResponse(data, { status });
519
679
  }
520
680
  };
521
681