bxo 0.0.5-dev.6 → 0.0.5-dev.60

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.
@@ -0,0 +1,438 @@
1
+ import type {
2
+ Route,
3
+ WSRoute,
4
+ Handler,
5
+ WebSocketHandler,
6
+ RouteConfig,
7
+ LifecycleHooks,
8
+ Plugin,
9
+ BXOOptions
10
+ } from '../types';
11
+ import { RequestHandler } from '../handlers/request-handler';
12
+
13
+ export default class BXO {
14
+ private _routes: Route[] = [];
15
+ private _wsRoutes: WSRoute[] = [];
16
+ private plugins: BXO[] = [];
17
+ private middleware: Plugin[] = [];
18
+ private hooks: LifecycleHooks = {};
19
+ private server?: any;
20
+ private isRunning: boolean = false;
21
+ private serverPort?: number;
22
+ private serverHostname?: string;
23
+ private enableValidation: boolean = true;
24
+ private requestHandler: RequestHandler;
25
+
26
+ constructor(options?: BXOOptions) {
27
+ this.enableValidation = options?.enableValidation ?? true;
28
+ this.requestHandler = new RequestHandler(
29
+ this._routes,
30
+ this._wsRoutes,
31
+ this.plugins,
32
+ this.middleware,
33
+ this.hooks,
34
+ this.enableValidation
35
+ );
36
+ }
37
+
38
+ // Lifecycle hook methods
39
+ onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
40
+ this.hooks.onBeforeStart = handler;
41
+ return this;
42
+ }
43
+
44
+ onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
45
+ this.hooks.onAfterStart = handler;
46
+ return this;
47
+ }
48
+
49
+ onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
50
+ this.hooks.onBeforeStop = handler;
51
+ return this;
52
+ }
53
+
54
+ onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
55
+ this.hooks.onAfterStop = handler;
56
+ return this;
57
+ }
58
+
59
+ onRequest(handler: (ctx: any, instance: BXO) => Promise<void> | void): this {
60
+ this.hooks.onRequest = handler;
61
+ return this;
62
+ }
63
+
64
+ onResponse(handler: (ctx: any, response: any, instance: BXO) => Promise<any> | any): this {
65
+ this.hooks.onResponse = handler;
66
+ return this;
67
+ }
68
+
69
+ onError(handler: (ctx: any, error: Error, instance: BXO) => Promise<any> | any): this {
70
+ this.hooks.onError = handler;
71
+ return this;
72
+ }
73
+
74
+ // Plugin system - now accepts both BXO instances and middleware plugins
75
+ use(plugin: BXO | Plugin): this {
76
+ if ('_routes' in plugin) {
77
+ // It's a BXO instance
78
+ this.plugins.push(plugin);
79
+ } else {
80
+ // It's a middleware plugin
81
+ this.middleware.push(plugin);
82
+ }
83
+ this.updateRequestHandler();
84
+ return this;
85
+ }
86
+
87
+ // HTTP method handlers with overloads for type safety
88
+ get<TConfig extends RouteConfig = {}>(
89
+ path: string,
90
+ handler: Handler<TConfig>
91
+ ): this;
92
+ get<TConfig extends RouteConfig = {}>(
93
+ path: string,
94
+ handler: Handler<TConfig>,
95
+ config: TConfig
96
+ ): this;
97
+ get<TConfig extends RouteConfig = {}>(
98
+ path: string,
99
+ handler: Handler<TConfig>,
100
+ config?: TConfig
101
+ ): this {
102
+ this._routes.push({ method: 'GET', path, handler, config });
103
+ this.updateRequestHandler();
104
+ return this;
105
+ }
106
+
107
+ post<TConfig extends RouteConfig = {}>(
108
+ path: string,
109
+ handler: Handler<TConfig>
110
+ ): this;
111
+ post<TConfig extends RouteConfig = {}>(
112
+ path: string,
113
+ handler: Handler<TConfig>,
114
+ config: TConfig
115
+ ): this;
116
+ post<TConfig extends RouteConfig = {}>(
117
+ path: string,
118
+ handler: Handler<TConfig>,
119
+ config?: TConfig
120
+ ): this {
121
+ this._routes.push({ method: 'POST', path, handler, config });
122
+ this.updateRequestHandler();
123
+ return this;
124
+ }
125
+
126
+ put<TConfig extends RouteConfig = {}>(
127
+ path: string,
128
+ handler: Handler<TConfig>
129
+ ): this;
130
+ put<TConfig extends RouteConfig = {}>(
131
+ path: string,
132
+ handler: Handler<TConfig>,
133
+ config: TConfig
134
+ ): this;
135
+ put<TConfig extends RouteConfig = {}>(
136
+ path: string,
137
+ handler: Handler<TConfig>,
138
+ config?: TConfig
139
+ ): this {
140
+ this._routes.push({ method: 'PUT', path, handler, config });
141
+ this.updateRequestHandler();
142
+ return this;
143
+ }
144
+
145
+ delete<TConfig extends RouteConfig = {}>(
146
+ path: string,
147
+ handler: Handler<TConfig>
148
+ ): this;
149
+ delete<TConfig extends RouteConfig = {}>(
150
+ path: string,
151
+ handler: Handler<TConfig>,
152
+ config: TConfig
153
+ ): this;
154
+ delete<TConfig extends RouteConfig = {}>(
155
+ path: string,
156
+ handler: Handler<TConfig>,
157
+ config?: TConfig
158
+ ): this {
159
+ this._routes.push({ method: 'DELETE', path, handler, config });
160
+ this.updateRequestHandler();
161
+ return this;
162
+ }
163
+
164
+ patch<TConfig extends RouteConfig = {}>(
165
+ path: string,
166
+ handler: Handler<TConfig>
167
+ ): this;
168
+ patch<TConfig extends RouteConfig = {}>(
169
+ path: string,
170
+ handler: Handler<TConfig>,
171
+ config: TConfig
172
+ ): this;
173
+ patch<TConfig extends RouteConfig = {}>(
174
+ path: string,
175
+ handler: Handler<TConfig>,
176
+ config?: TConfig
177
+ ): this {
178
+ this._routes.push({ method: 'PATCH', path, handler, config });
179
+ this.updateRequestHandler();
180
+ return this;
181
+ }
182
+
183
+ // WebSocket route handler
184
+ ws(path: string, handler: WebSocketHandler): this {
185
+ this._wsRoutes.push({ path, handler });
186
+ this.updateRequestHandler();
187
+ return this;
188
+ }
189
+
190
+ // Update the request handler with current routes and configuration
191
+ private updateRequestHandler(): void {
192
+ this.requestHandler = new RequestHandler(
193
+ this.getAllRoutes(),
194
+ this.getAllWSRoutes(),
195
+ this.plugins,
196
+ this.middleware,
197
+ this.hooks,
198
+ this.enableValidation
199
+ );
200
+ }
201
+
202
+ // Helper methods to get all routes including plugin routes
203
+ private getAllRoutes(): Route[] {
204
+ const allRoutes = [...this._routes];
205
+ for (const plugin of this.plugins) {
206
+ allRoutes.push(...plugin._routes);
207
+ }
208
+ return allRoutes;
209
+ }
210
+
211
+ private getAllWSRoutes(): WSRoute[] {
212
+ const allWSRoutes = [...this._wsRoutes];
213
+ for (const plugin of this.plugins) {
214
+ allWSRoutes.push(...plugin._wsRoutes);
215
+ }
216
+ return allWSRoutes;
217
+ }
218
+
219
+ // Server management methods
220
+ async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
221
+ if (this.isRunning) {
222
+ return;
223
+ }
224
+
225
+ try {
226
+ // Before start hook
227
+ if (this.hooks.onBeforeStart) {
228
+ await this.hooks.onBeforeStart(this);
229
+ }
230
+
231
+ this.server = Bun.serve({
232
+ port,
233
+ hostname,
234
+ fetch: (request, server) => this.requestHandler.handleRequest(request, server),
235
+ websocket: {
236
+ message: (ws: any, message: any) => {
237
+ const handler = ws.data?.handler;
238
+ if (handler?.onMessage) {
239
+ handler.onMessage(ws, message);
240
+ }
241
+ },
242
+ open: (ws: any) => {
243
+ const handler = ws.data?.handler;
244
+ if (handler?.onOpen) {
245
+ handler.onOpen(ws);
246
+ }
247
+ },
248
+ close: (ws: any, code?: number, reason?: string) => {
249
+ const handler = ws.data?.handler;
250
+ if (handler?.onClose) {
251
+ handler.onClose(ws, code, reason);
252
+ }
253
+ }
254
+ }
255
+ });
256
+
257
+ // Verify server was created successfully
258
+ if (!this.server) {
259
+ throw new Error('Failed to create server instance');
260
+ }
261
+
262
+ this.isRunning = true;
263
+ this.serverPort = port;
264
+ this.serverHostname = hostname;
265
+
266
+ // After start hook
267
+ if (this.hooks.onAfterStart) {
268
+ await this.hooks.onAfterStart(this);
269
+ }
270
+
271
+ // Handle graceful shutdown
272
+ const shutdownHandler = async () => {
273
+ await this.stop();
274
+ process.exit(0);
275
+ };
276
+
277
+ process.on('SIGINT', shutdownHandler);
278
+ process.on('SIGTERM', shutdownHandler);
279
+
280
+ } catch (error) {
281
+ console.error('❌ Failed to start server:', error);
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ async stop(): Promise<void> {
287
+ if (!this.isRunning) {
288
+ return;
289
+ }
290
+
291
+ try {
292
+ // Before stop hook
293
+ if (this.hooks.onBeforeStop) {
294
+ await this.hooks.onBeforeStop(this);
295
+ }
296
+
297
+ if (this.server) {
298
+ try {
299
+ // Try to stop the server gracefully
300
+ if (typeof this.server.stop === 'function') {
301
+ this.server.stop();
302
+ } else {
303
+ console.warn('⚠️ Server stop method not available');
304
+ }
305
+ } catch (stopError) {
306
+ console.error('❌ Error calling server.stop():', stopError);
307
+ }
308
+
309
+ // Clear the server reference
310
+ this.server = undefined;
311
+ }
312
+
313
+ // Reset state regardless of server.stop() success
314
+ this.isRunning = false;
315
+ this.serverPort = undefined;
316
+ this.serverHostname = undefined;
317
+
318
+ // After stop hook
319
+ if (this.hooks.onAfterStop) {
320
+ await this.hooks.onAfterStop(this);
321
+ }
322
+
323
+ } catch (error) {
324
+ console.error('❌ Error stopping server:', error);
325
+ // Even if there's an error, reset the state
326
+ this.isRunning = false;
327
+ this.server = undefined;
328
+ this.serverPort = undefined;
329
+ this.serverHostname = undefined;
330
+ throw error;
331
+ }
332
+ }
333
+
334
+ // Backward compatibility
335
+ async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
336
+ return this.start(port, hostname);
337
+ }
338
+
339
+ // Server status
340
+ isServerRunning(): boolean {
341
+ return this.isRunning && this.server !== undefined;
342
+ }
343
+
344
+ getServerInfo(): { running: boolean } {
345
+ return {
346
+ running: this.isRunning
347
+ };
348
+ }
349
+
350
+ // Get server information (alias for getServerInfo)
351
+ get info() {
352
+ // Calculate total routes including plugins
353
+ const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
354
+ const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
355
+
356
+ return {
357
+ // Server status
358
+ running: this.isRunning,
359
+ server: this.server ? 'Bun' : null,
360
+
361
+ // Connection details
362
+ hostname: this.serverHostname,
363
+ port: this.serverPort,
364
+ url: this.isRunning && this.serverHostname && this.serverPort
365
+ ? `http://${this.serverHostname}:${this.serverPort}`
366
+ : null,
367
+
368
+ // Application statistics
369
+ totalRoutes,
370
+ totalWsRoutes,
371
+ totalPlugins: this.plugins.length,
372
+
373
+ // System information
374
+ runtime: 'Bun',
375
+ version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
376
+ pid: process.pid,
377
+ uptime: this.isRunning ? process.uptime() : 0
378
+ };
379
+ }
380
+
381
+ // Get all routes information
382
+ get routes() {
383
+ // Get routes from main instance
384
+ const mainRoutes = this._routes.map((route: Route) => ({
385
+ method: route.method,
386
+ path: route.path,
387
+ hasConfig: !!route.config,
388
+ config: route.config || null,
389
+ source: 'main' as const
390
+ }));
391
+
392
+ // Get routes from all plugins
393
+ const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
394
+ plugin._routes.map((route: Route) => ({
395
+ method: route.method,
396
+ path: route.path,
397
+ hasConfig: !!route.config,
398
+ config: route.config || null,
399
+ source: 'plugin' as const,
400
+ pluginIndex
401
+ }))
402
+ );
403
+
404
+ return [...mainRoutes, ...pluginRoutes];
405
+ }
406
+
407
+ // Get all WebSocket routes information
408
+ get wsRoutes() {
409
+ // Get WebSocket routes from main instance
410
+ const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
411
+ path: route.path,
412
+ hasHandlers: {
413
+ onOpen: !!route.handler.onOpen,
414
+ onMessage: !!route.handler.onMessage,
415
+ onClose: !!route.handler.onClose,
416
+ onError: !!route.handler.onError
417
+ },
418
+ source: 'main' as const
419
+ }));
420
+
421
+ // Get WebSocket routes from all plugins
422
+ const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
423
+ plugin._wsRoutes.map((route: WSRoute) => ({
424
+ path: route.path,
425
+ hasHandlers: {
426
+ onOpen: !!route.handler.onOpen,
427
+ onMessage: !!route.handler.onMessage,
428
+ onClose: !!route.handler.onClose,
429
+ onError: !!route.handler.onError
430
+ },
431
+ source: 'plugin' as const,
432
+ pluginIndex
433
+ }))
434
+ );
435
+
436
+ return [...mainWsRoutes, ...pluginWsRoutes];
437
+ }
438
+ }
@@ -0,0 +1,229 @@
1
+ import type { Context, Route, WSRoute, Plugin, LifecycleHooks } from '../types';
2
+ import { parseQuery, parseHeaders, parseCookies, parseRequestBody } from '../utils';
3
+ import { matchRoute, matchWSRoute } from '../utils/route-matcher';
4
+ import { createContext, createOptionsContext, getInternalCookies } from '../utils/context-factory';
5
+ import { processResponse, createValidationErrorResponse, createErrorResponse } from '../utils/response-handler';
6
+
7
+ export class RequestHandler {
8
+ constructor(
9
+ private routes: Route[],
10
+ private wsRoutes: WSRoute[],
11
+ private plugins: any[],
12
+ private middleware: Plugin[],
13
+ private hooks: LifecycleHooks,
14
+ private enableValidation: boolean
15
+ ) { }
16
+
17
+ async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
18
+ const url = new URL(request.url);
19
+ const method = request.method;
20
+ const rawPathname = url.pathname;
21
+ let pathname: string;
22
+
23
+ try {
24
+ pathname = decodeURI(rawPathname);
25
+ } catch {
26
+ pathname = rawPathname;
27
+ }
28
+
29
+ // Check for WebSocket upgrade
30
+ if (request.headers.get('upgrade') === 'websocket') {
31
+ return this.handleWebSocketUpgrade(request, pathname, server);
32
+ }
33
+
34
+ // Handle OPTIONS requests for CORS preflight before route matching
35
+ if (method === 'OPTIONS') {
36
+ return this.handleOptionsRequest(request, pathname);
37
+ }
38
+
39
+ // Find matching route
40
+ const matchResult = matchRoute(method, pathname, this.routes);
41
+ if (!matchResult) {
42
+ return new Response('Not Found', { status: 404 });
43
+ }
44
+
45
+ const { route, params } = matchResult;
46
+ let ctx: Context | null = null
47
+
48
+ try {
49
+ // Parse request data
50
+ const query = parseQuery(url.searchParams);
51
+ const headers = parseHeaders(request.headers);
52
+ const cookies = parseCookies(request.headers.get('cookie'));
53
+ const body = await parseRequestBody(request);
54
+
55
+ // Create context
56
+ ctx = createContext(
57
+ params,
58
+ query,
59
+ body,
60
+ headers,
61
+ cookies,
62
+ pathname,
63
+ request,
64
+ route.config,
65
+ this.enableValidation
66
+ );
67
+
68
+ // Run middleware and hooks
69
+ await this.runRequestHooks(ctx);
70
+
71
+ // Execute route handler
72
+ let response = await route.handler(ctx);
73
+
74
+ // Run response hooks
75
+ response = await this.runResponseHooks(ctx, response);
76
+
77
+ // Process and return response
78
+ const internalCookies = getInternalCookies(ctx);
79
+ return processResponse(response, ctx, internalCookies, this.enableValidation, route.config);
80
+
81
+ } catch (error) {
82
+ // Run error hooks
83
+ const errorResponse = await this.runErrorHooks(ctx as Context, error as Error);
84
+ if (errorResponse) {
85
+ return errorResponse;
86
+ }
87
+
88
+ // Default error response
89
+ if (error instanceof Error && ('errors' in error || 'issues' in error)) {
90
+ return createValidationErrorResponse(error, 400);
91
+ }
92
+
93
+ return createErrorResponse(error as Error);
94
+ }
95
+ }
96
+
97
+ private handleWebSocketUpgrade(request: Request, pathname: string, server?: any): Response | undefined {
98
+ const wsMatchResult = matchWSRoute(pathname, this.wsRoutes);
99
+ if (wsMatchResult && server) {
100
+ const success = server.upgrade(request, {
101
+ data: {
102
+ handler: wsMatchResult.route.handler,
103
+ params: wsMatchResult.params,
104
+ pathname
105
+ }
106
+ });
107
+
108
+ if (success) {
109
+ return; // undefined response means upgrade was successful
110
+ }
111
+ }
112
+ return new Response('WebSocket upgrade failed', { status: 400 });
113
+ }
114
+
115
+ private async handleOptionsRequest(request: Request, pathname: string): Promise<Response> {
116
+ const headers = parseHeaders(request.headers);
117
+ const ctx = createOptionsContext(pathname, request, headers);
118
+
119
+ // Run middleware onRequest hooks for OPTIONS requests
120
+ for (const plugin of this.middleware) {
121
+ if (plugin.onRequest) {
122
+ const result = await plugin.onRequest(ctx);
123
+ if (result instanceof Response) {
124
+ return result;
125
+ }
126
+ }
127
+ }
128
+
129
+ // Run global onRequest hook
130
+ if (this.hooks.onRequest) {
131
+ const result = await this.hooks.onRequest(ctx, this) as any
132
+ if (result instanceof Response) {
133
+ return result;
134
+ }
135
+ }
136
+
137
+ // Run BXO instance onRequest hooks
138
+ for (const bxoInstance of this.plugins) {
139
+ if (bxoInstance.hooks?.onRequest) {
140
+ const result = await bxoInstance.hooks.onRequest(ctx, this);
141
+ if (result instanceof Response) {
142
+ return result;
143
+ }
144
+ }
145
+ }
146
+
147
+ // If no middleware handled the OPTIONS request, return a default response
148
+ return new Response(null, {
149
+ status: 204,
150
+ headers: ctx.set.headers || {}
151
+ });
152
+ }
153
+
154
+ private async runRequestHooks(ctx: Context): Promise<void> {
155
+ // Run middleware onRequest hooks
156
+ for (const plugin of this.middleware) {
157
+ if (plugin.onRequest) {
158
+ await plugin.onRequest(ctx);
159
+ }
160
+ }
161
+
162
+ // Run global onRequest hook
163
+ if (this.hooks.onRequest) {
164
+ await this.hooks.onRequest(ctx, this);
165
+ }
166
+
167
+ // Run BXO instance onRequest hooks
168
+ for (const bxoInstance of this.plugins) {
169
+ if (bxoInstance.hooks?.onRequest) {
170
+ await bxoInstance.hooks.onRequest(ctx, this);
171
+ }
172
+ }
173
+ }
174
+
175
+ private async runResponseHooks(ctx: Context, response: any): Promise<any> {
176
+ // Run middleware onResponse hooks
177
+ for (const plugin of this.middleware) {
178
+ if (plugin.onResponse) {
179
+ response = await plugin.onResponse(ctx, response) || response;
180
+ }
181
+ }
182
+
183
+ // Run global onResponse hook
184
+ if (this.hooks.onResponse) {
185
+ response = await this.hooks.onResponse(ctx, response, this) || response;
186
+ }
187
+
188
+ // Run BXO instance onResponse hooks
189
+ for (const bxoInstance of this.plugins) {
190
+ if (bxoInstance.hooks?.onResponse) {
191
+ response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
192
+ }
193
+ }
194
+
195
+ return response;
196
+ }
197
+
198
+ private async runErrorHooks(ctx: Context, error: Error): Promise<Response | null> {
199
+ let errorResponse: any = null;
200
+
201
+ // Run middleware onError hooks
202
+ for (const plugin of this.middleware) {
203
+ if (plugin.onError) {
204
+ errorResponse = await plugin.onError(ctx, error) || errorResponse;
205
+ }
206
+ }
207
+
208
+ // Run global onError hook
209
+ if (this.hooks.onError) {
210
+ errorResponse = await this.hooks.onError(ctx, error, this);
211
+ }
212
+
213
+ // Run BXO instance onError hooks
214
+ for (const bxoInstance of this.plugins) {
215
+ if (bxoInstance.hooks?.onError) {
216
+ errorResponse = await bxoInstance.hooks.onError(ctx, error, this) || errorResponse;
217
+ }
218
+ }
219
+
220
+ if (errorResponse) {
221
+ if (errorResponse instanceof Response) {
222
+ return errorResponse;
223
+ }
224
+ return createErrorResponse(errorResponse, 500);
225
+ }
226
+
227
+ return null;
228
+ }
229
+ }