bxo 0.0.5-dev.3 → 0.0.5-dev.5
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/index.ts +128 -10
- package/package.json +1 -1
package/index.ts
CHANGED
@@ -3,6 +3,18 @@ import { z } from 'zod';
|
|
3
3
|
// Type utilities for extracting types from Zod schemas
|
4
4
|
type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
|
5
5
|
|
6
|
+
// OpenAPI detail information
|
7
|
+
interface RouteDetail {
|
8
|
+
summary?: string;
|
9
|
+
description?: string;
|
10
|
+
tags?: string[];
|
11
|
+
operationId?: string;
|
12
|
+
deprecated?: boolean;
|
13
|
+
produces?: string[];
|
14
|
+
consumes?: string[];
|
15
|
+
[key: string]: any; // Allow additional OpenAPI properties
|
16
|
+
}
|
17
|
+
|
6
18
|
// Configuration interface for route handlers
|
7
19
|
interface RouteConfig {
|
8
20
|
params?: z.ZodSchema<any>;
|
@@ -10,6 +22,7 @@ interface RouteConfig {
|
|
10
22
|
body?: z.ZodSchema<any>;
|
11
23
|
headers?: z.ZodSchema<any>;
|
12
24
|
response?: z.ZodSchema<any>;
|
25
|
+
detail?: RouteDetail;
|
13
26
|
}
|
14
27
|
|
15
28
|
// Context type that's fully typed based on the route configuration
|
@@ -39,6 +52,20 @@ interface Route {
|
|
39
52
|
config?: RouteConfig;
|
40
53
|
}
|
41
54
|
|
55
|
+
// WebSocket handler interface
|
56
|
+
interface WebSocketHandler {
|
57
|
+
onOpen?: (ws: any) => void;
|
58
|
+
onMessage?: (ws: any, message: string | Buffer) => void;
|
59
|
+
onClose?: (ws: any, code?: number, reason?: string) => void;
|
60
|
+
onError?: (ws: any, error: Error) => void;
|
61
|
+
}
|
62
|
+
|
63
|
+
// WebSocket route definition
|
64
|
+
interface WSRoute {
|
65
|
+
path: string;
|
66
|
+
handler: WebSocketHandler;
|
67
|
+
}
|
68
|
+
|
42
69
|
// Lifecycle hooks
|
43
70
|
interface LifecycleHooks {
|
44
71
|
onBeforeStart?: () => Promise<void> | void;
|
@@ -54,6 +81,7 @@ interface LifecycleHooks {
|
|
54
81
|
|
55
82
|
export default class BXO {
|
56
83
|
private _routes: Route[] = [];
|
84
|
+
private _wsRoutes: WSRoute[] = [];
|
57
85
|
private plugins: BXO[] = [];
|
58
86
|
private hooks: LifecycleHooks = {};
|
59
87
|
private server?: any;
|
@@ -209,6 +237,12 @@ export default class BXO {
|
|
209
237
|
return this;
|
210
238
|
}
|
211
239
|
|
240
|
+
// WebSocket route handler
|
241
|
+
ws(path: string, handler: WebSocketHandler): this {
|
242
|
+
this._wsRoutes.push({ path, handler });
|
243
|
+
return this;
|
244
|
+
}
|
245
|
+
|
212
246
|
// Route matching utility
|
213
247
|
private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
|
214
248
|
for (const route of this._routes) {
|
@@ -248,6 +282,43 @@ export default class BXO {
|
|
248
282
|
return null;
|
249
283
|
}
|
250
284
|
|
285
|
+
// WebSocket route matching utility
|
286
|
+
private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
|
287
|
+
for (const route of this._wsRoutes) {
|
288
|
+
const routeSegments = route.path.split('/').filter(Boolean);
|
289
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
290
|
+
|
291
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
292
|
+
|
293
|
+
const params: Record<string, string> = {};
|
294
|
+
let isMatch = true;
|
295
|
+
|
296
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
297
|
+
const routeSegment = routeSegments[i];
|
298
|
+
const pathSegment = pathSegments[i];
|
299
|
+
|
300
|
+
if (!routeSegment || !pathSegment) {
|
301
|
+
isMatch = false;
|
302
|
+
break;
|
303
|
+
}
|
304
|
+
|
305
|
+
if (routeSegment.startsWith(':')) {
|
306
|
+
const paramName = routeSegment.slice(1);
|
307
|
+
params[paramName] = decodeURIComponent(pathSegment);
|
308
|
+
} else if (routeSegment !== pathSegment) {
|
309
|
+
isMatch = false;
|
310
|
+
break;
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
if (isMatch) {
|
315
|
+
return { route, params };
|
316
|
+
}
|
317
|
+
}
|
318
|
+
|
319
|
+
return null;
|
320
|
+
}
|
321
|
+
|
251
322
|
// Parse query string
|
252
323
|
private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
|
253
324
|
const query: Record<string, string | undefined> = {};
|
@@ -273,11 +344,30 @@ export default class BXO {
|
|
273
344
|
}
|
274
345
|
|
275
346
|
// Main request handler
|
276
|
-
private async handleRequest(request: Request): Promise<Response> {
|
347
|
+
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
277
348
|
const url = new URL(request.url);
|
278
349
|
const method = request.method;
|
279
350
|
const pathname = url.pathname;
|
280
351
|
|
352
|
+
// Check for WebSocket upgrade
|
353
|
+
if (request.headers.get('upgrade') === 'websocket') {
|
354
|
+
const wsMatchResult = this.matchWSRoute(pathname);
|
355
|
+
if (wsMatchResult && server) {
|
356
|
+
const success = server.upgrade(request, {
|
357
|
+
data: {
|
358
|
+
handler: wsMatchResult.route.handler,
|
359
|
+
params: wsMatchResult.params,
|
360
|
+
pathname
|
361
|
+
}
|
362
|
+
});
|
363
|
+
|
364
|
+
if (success) {
|
365
|
+
return; // undefined response means upgrade was successful
|
366
|
+
}
|
367
|
+
}
|
368
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
369
|
+
}
|
370
|
+
|
281
371
|
const matchResult = this.matchRoute(method, pathname);
|
282
372
|
if (!matchResult) {
|
283
373
|
return new Response('Not Found', { status: 404 });
|
@@ -499,7 +589,27 @@ export default class BXO {
|
|
499
589
|
this.server = Bun.serve({
|
500
590
|
port,
|
501
591
|
hostname,
|
502
|
-
fetch: (request) => this.handleRequest(request),
|
592
|
+
fetch: (request, server) => this.handleRequest(request, server),
|
593
|
+
websocket: {
|
594
|
+
message: (ws: any, message: any) => {
|
595
|
+
const handler = ws.data?.handler;
|
596
|
+
if (handler?.onMessage) {
|
597
|
+
handler.onMessage(ws, message);
|
598
|
+
}
|
599
|
+
},
|
600
|
+
open: (ws: any) => {
|
601
|
+
const handler = ws.data?.handler;
|
602
|
+
if (handler?.onOpen) {
|
603
|
+
handler.onOpen(ws);
|
604
|
+
}
|
605
|
+
},
|
606
|
+
close: (ws: any, code?: number, reason?: string) => {
|
607
|
+
const handler = ws.data?.handler;
|
608
|
+
if (handler?.onClose) {
|
609
|
+
handler.onClose(ws, code, reason);
|
610
|
+
}
|
611
|
+
}
|
612
|
+
}
|
503
613
|
});
|
504
614
|
|
505
615
|
this.isRunning = true;
|
@@ -627,6 +737,7 @@ export default class BXO {
|
|
627
737
|
|
628
738
|
// Application statistics
|
629
739
|
totalRoutes: this._routes.length,
|
740
|
+
totalWsRoutes: this._wsRoutes.length,
|
630
741
|
totalPlugins: this.plugins.length,
|
631
742
|
|
632
743
|
// Hot reload configuration
|
@@ -648,13 +759,20 @@ export default class BXO {
|
|
648
759
|
method: route.method,
|
649
760
|
path: route.path,
|
650
761
|
hasConfig: !!route.config,
|
651
|
-
config: route.config
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
762
|
+
config: route.config || null
|
763
|
+
}));
|
764
|
+
}
|
765
|
+
|
766
|
+
// Get all WebSocket routes information
|
767
|
+
get wsRoutes() {
|
768
|
+
return this._wsRoutes.map((route: WSRoute) => ({
|
769
|
+
path: route.path,
|
770
|
+
hasHandlers: {
|
771
|
+
onOpen: !!route.handler.onOpen,
|
772
|
+
onMessage: !!route.handler.onMessage,
|
773
|
+
onClose: !!route.handler.onClose,
|
774
|
+
onError: !!route.handler.onError
|
775
|
+
}
|
658
776
|
}));
|
659
777
|
}
|
660
778
|
}
|
@@ -667,4 +785,4 @@ const error = (error: Error, status: number = 500) => {
|
|
667
785
|
export { z, error };
|
668
786
|
|
669
787
|
// Export types for external use
|
670
|
-
export type { RouteConfig, Handler };
|
788
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
|