@asenajs/ergenecore 1.0.0

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +636 -0
  3. package/dist/index.d.ts +24 -0
  4. package/dist/index.js +25 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/Ergenecore.d.ts +598 -0
  7. package/dist/lib/Ergenecore.js +1406 -0
  8. package/dist/lib/Ergenecore.js.map +1 -0
  9. package/dist/lib/ErgenecoreContextWrapper.d.ts +190 -0
  10. package/dist/lib/ErgenecoreContextWrapper.js +387 -0
  11. package/dist/lib/ErgenecoreContextWrapper.js.map +1 -0
  12. package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +68 -0
  13. package/dist/lib/ErgenecoreWebsocketAdapter.js +233 -0
  14. package/dist/lib/ErgenecoreWebsocketAdapter.js.map +1 -0
  15. package/dist/lib/defaults/ConfigService.d.ts +39 -0
  16. package/dist/lib/defaults/ConfigService.js +30 -0
  17. package/dist/lib/defaults/ConfigService.js.map +1 -0
  18. package/dist/lib/defaults/MiddlewareService.d.ts +39 -0
  19. package/dist/lib/defaults/MiddlewareService.js +30 -0
  20. package/dist/lib/defaults/MiddlewareService.js.map +1 -0
  21. package/dist/lib/defaults/StaticServeService.d.ts +24 -0
  22. package/dist/lib/defaults/StaticServeService.js +23 -0
  23. package/dist/lib/defaults/StaticServeService.js.map +1 -0
  24. package/dist/lib/defaults/ValidationService.d.ts +35 -0
  25. package/dist/lib/defaults/ValidationService.js +34 -0
  26. package/dist/lib/defaults/ValidationService.js.map +1 -0
  27. package/dist/lib/defaults/index.d.ts +25 -0
  28. package/dist/lib/defaults/index.js +26 -0
  29. package/dist/lib/defaults/index.js.map +1 -0
  30. package/dist/lib/errors.d.ts +84 -0
  31. package/dist/lib/errors.js +108 -0
  32. package/dist/lib/errors.js.map +1 -0
  33. package/dist/lib/index.d.ts +36 -0
  34. package/dist/lib/index.js +37 -0
  35. package/dist/lib/index.js.map +1 -0
  36. package/dist/lib/middlewares/CorsMiddleware.d.ts +147 -0
  37. package/dist/lib/middlewares/CorsMiddleware.js +163 -0
  38. package/dist/lib/middlewares/CorsMiddleware.js.map +1 -0
  39. package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +330 -0
  40. package/dist/lib/middlewares/RateLimiterMiddleware.js +300 -0
  41. package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -0
  42. package/dist/lib/types/Handler.d.ts +65 -0
  43. package/dist/lib/types/Handler.js +10 -0
  44. package/dist/lib/types/Handler.js.map +1 -0
  45. package/dist/lib/types/StaticServeExtras.d.ts +29 -0
  46. package/dist/lib/types/StaticServeExtras.js +2 -0
  47. package/dist/lib/types/StaticServeExtras.js.map +1 -0
  48. package/dist/lib/types/Validation.d.ts +7 -0
  49. package/dist/lib/types/Validation.js +2 -0
  50. package/dist/lib/types/Validation.js.map +1 -0
  51. package/dist/lib/types/index.d.ts +24 -0
  52. package/dist/lib/types/index.js +25 -0
  53. package/dist/lib/types/index.js.map +1 -0
  54. package/dist/lib/utils/factory.d.ts +121 -0
  55. package/dist/lib/utils/factory.js +144 -0
  56. package/dist/lib/utils/factory.js.map +1 -0
  57. package/package.json +47 -0
@@ -0,0 +1,1406 @@
1
+ import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
2
+ import { blue, green, red, yellow } from '@asenajs/asena/logger';
3
+ import { shouldApplyMiddleware } from '@asenajs/asena/utils/patternMatcher';
4
+ import { ErgenecoreWebsocketAdapter } from './ErgenecoreWebsocketAdapter';
5
+ import { ErgenecoreContextWrapper } from './ErgenecoreContextWrapper';
6
+ import * as Bun from 'bun';
7
+ import * as path from 'path';
8
+ import { HttpException, MiddlewareResponseError } from './errors';
9
+ /**
10
+ * Static response headers for performance
11
+ *
12
+ * Pre-allocated header objects to avoid creating new objects for each response.
13
+ * This reduces garbage collection pressure and improves performance.
14
+ */
15
+ const STATIC_JSON_HEADERS = Object.freeze({ 'Content-Type': 'application/json' });
16
+ /**
17
+ * CoreAdapter - Native Bun adapter for Asenajs
18
+ *
19
+ * High-performance HTTP adapter using Bun's native APIs exclusively:
20
+ * - SIMD-accelerated routing
21
+ * - Native Request/Response
22
+ * - Zero framework overhead
23
+ * - Built-in parameter extraction
24
+ */
25
+ export class Ergenecore extends AsenaAdapter {
26
+ /**
27
+ * Adapter name
28
+ */
29
+ name = 'Ergenecore';
30
+ /**
31
+ * Server port (inherited from AsenaAdapter but needs override for initialization)
32
+ */
33
+ port = 3000;
34
+ /**
35
+ * Server hostname
36
+ */
37
+ _hostname;
38
+ /**
39
+ * Bun server instance
40
+ */
41
+ server;
42
+ /**
43
+ * Route queue for deferred registration
44
+ * Routes are queued during initialization and built when server starts
45
+ */
46
+ routeQueue = [];
47
+ /**
48
+ * WebSocket route queue for deferred registration
49
+ * WebSocket routes are queued during initialization and built when server starts
50
+ */
51
+ wsRouteQueue = [];
52
+ /**
53
+ * Flag to track if routes have been built
54
+ */
55
+ routesBuilt = false;
56
+ /**
57
+ * Error handler function
58
+ */
59
+ errorHandler;
60
+ /**
61
+ * Global middlewares with route configuration
62
+ * Structure: Array<{ middleware, config }>
63
+ * Config is optional - if not provided, middleware applies to all routes
64
+ */
65
+ globalMiddlewares = [];
66
+ options = {};
67
+ /**
68
+ * Creates a new CoreAdapter instance
69
+ *
70
+ * @param logger - Server logger instance
71
+ * @param websocketAdapter - WebSocket adapter instance (optional)
72
+ */
73
+ constructor(logger, websocketAdapter) {
74
+ // Call parent constructor with logger and websocketAdapter
75
+ super(logger, websocketAdapter || new ErgenecoreWebsocketAdapter(logger));
76
+ }
77
+ /**
78
+ * Registers a WebSocket route (deferred registration)
79
+ *
80
+ * Routes are queued and will be built into Bun's native router format
81
+ * when the server starts. Supports middleware chain execution before
82
+ * WebSocket upgrade.
83
+ *
84
+ * @param params - WebSocket route parameters (path, middlewares, websocketService)
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * adapter.registerWebsocketRoute({
89
+ * path: '/chat',
90
+ * middlewares: [authMiddleware],
91
+ * websocketService: chatService
92
+ * });
93
+ * ```
94
+ */
95
+ registerWebsocketRoute(params) {
96
+ // Queue WebSocket route for building during start()
97
+ this.wsRouteQueue.push(params);
98
+ // Register WebSocket service with adapter using the route path (not namespace)
99
+ if (this.websocketAdapter && params.websocketService) {
100
+ this.websocketAdapter.registerWebSocket(params.websocketService);
101
+ }
102
+ }
103
+ /**
104
+ * Sets the server port
105
+ *
106
+ * @param port - Port number
107
+ */
108
+ setPort(port) {
109
+ this.port = port;
110
+ }
111
+ /**
112
+ * Registers a global middleware with optional pattern matching
113
+ *
114
+ * @param middleware - Middleware instance
115
+ * @param config - Optional route configuration for pattern matching
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * // Old API (still supported) - applies to all routes
120
+ * adapter.use(loggerMiddleware);
121
+ *
122
+ * // New API with pattern matching - applies only to matching routes
123
+ * adapter.use(authMiddleware, {
124
+ * include: ['/api/*', '/admin/*'],
125
+ * exclude: ['/api/health']
126
+ * });
127
+ * ```
128
+ */
129
+ use(middleware, config) {
130
+ this.globalMiddlewares.push({ middleware, config });
131
+ }
132
+ /**
133
+ * Registers a route (deferred registration)
134
+ *
135
+ * Routes are queued and will be built into Bun's native router format
136
+ * when the server starts.
137
+ *
138
+ * @param params - Route parameters
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * adapter.registerRoute({
143
+ * method: HttpMethod.GET,
144
+ * path: '/users/:id',
145
+ * middlewares: [],
146
+ * handler: async (ctx) => ctx.send({ id: ctx.getParam('id') })
147
+ * });
148
+ * ```
149
+ */
150
+ registerRoute(params) {
151
+ this.routeQueue.push(params);
152
+ }
153
+ /**
154
+ * Starts the Bun server
155
+ *
156
+ * Builds routes from queue (HTTP + WebSocket) and starts Bun.serve() with native router.
157
+ *
158
+ * Process:
159
+ * 1. Build HTTP routes from route queue
160
+ * 2. Build WebSocket routes from WebSocket route queue
161
+ * 3. Check for path collisions (HTTP GET vs WebSocket GET)
162
+ * 4. Merge HTTP and WebSocket routes
163
+ * 5. Prepare WebSocket adapter
164
+ * 6. Start Bun server with merged routes
165
+ *
166
+ * @param port - Optional port to override default
167
+ * @returns Bun server instance
168
+ */
169
+ start(port) {
170
+ // Build routes if not built yet
171
+ const serverHostname = this._hostname;
172
+ if (!this.routesBuilt) {
173
+ // 1. Build HTTP routes
174
+ const httpRoutes = this.buildBunRoutes();
175
+ // 2. Build WebSocket routes
176
+ const wsRoutes = this.buildWebSocketRoutes();
177
+ // 3. Check for path collisions
178
+ this.checkPathCollisions(httpRoutes, wsRoutes);
179
+ // 4. Merge routes
180
+ const finalRoutes = this.mergeRoutes(httpRoutes, wsRoutes);
181
+ // 5. Prepare WebSocket before starting server
182
+ this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
183
+ const serverPort = port ?? this.port;
184
+ // 6. Start Bun server with merged routes
185
+ this.server = Bun.serve({
186
+ ...this.options.serveOptions,
187
+ port: serverPort,
188
+ hostname: serverHostname,
189
+ routes: finalRoutes,
190
+ websocket: this.websocketAdapter.websocket,
191
+ });
192
+ // Start WebSocket server (initializes AsenaWebSocketServer for each namespace)
193
+ this.websocketAdapter.startWebsocket(this.server);
194
+ this.routesBuilt = true;
195
+ // Log controller summary first
196
+ if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
197
+ this.logControllerSummary();
198
+ // Then log detailed route list
199
+ this.logger.info(this.buildControllerBasedLog());
200
+ }
201
+ else {
202
+ this.logger.info('No routes registered');
203
+ }
204
+ }
205
+ const hostDisplay = serverHostname || 'localhost';
206
+ this.logger.info(`Server ready → http://${hostDisplay}:${this.server.port}`);
207
+ return this.server;
208
+ }
209
+ /**
210
+ * Stops the server
211
+ *
212
+ * @param closeActiveConnections - Whether to close active connections
213
+ */
214
+ async stop(closeActiveConnections = true) {
215
+ if (this.server) {
216
+ await this.server.stop(closeActiveConnections);
217
+ this.logger.info('Server stopped');
218
+ }
219
+ }
220
+ /**
221
+ * Sets the error handler
222
+ *
223
+ * @param errorHandler - Error handler function
224
+ */
225
+ onError(errorHandler) {
226
+ this.errorHandler = errorHandler;
227
+ }
228
+ /**
229
+ * Sets serve options
230
+ *
231
+ * @param options - Serve options function
232
+ */
233
+ async serveOptions(options) {
234
+ this.options = await options();
235
+ }
236
+ /**
237
+ * Builds Bun native router object from queued routes
238
+ *
239
+ * Optimizations applied:
240
+ * 1. Base path extraction - Groups routes by static base path
241
+ * 2. Common middleware detection - Identifies shared middlewares per group
242
+ * 3. Middleware deduplication - Reduces redundant middleware execution
243
+ *
244
+ * Converts Asena route format to Bun's native router format:
245
+ * ```typescript
246
+ * {
247
+ * "/users": {
248
+ * GET: (req) => Response,
249
+ * POST: (req) => Response
250
+ * },
251
+ * "/users/:id": {
252
+ * GET: (req) => Response
253
+ * }
254
+ * }
255
+ * ```
256
+ *
257
+ * @returns Bun router object
258
+ */
259
+ buildBunRoutes() {
260
+ const routes = {};
261
+ // Group routes by base path for optimization
262
+ const routeGroups = this.groupRoutesByBasePath(this.routeQueue);
263
+ // Process each base path group
264
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
265
+ for (const [_, groupRoutes] of routeGroups) {
266
+ // Extract common middlewares for this group
267
+ const commonMiddlewares = this.extractCommonMiddlewares(groupRoutes);
268
+ // Group routes by exact path (same as before)
269
+ const routesByPath = new Map();
270
+ for (const route of groupRoutes) {
271
+ if (!routesByPath.has(route.path)) {
272
+ routesByPath.set(route.path, []);
273
+ }
274
+ routesByPath.get(route.path).push(route);
275
+ }
276
+ // Build Bun router object for each path
277
+ for (const [path, pathRoutes] of routesByPath) {
278
+ routes[path] = {};
279
+ for (const route of pathRoutes) {
280
+ const method = route.method.toUpperCase();
281
+ // Fast Path Optimization with Pattern Matching
282
+ // ✅ Check if this route has any applicable global middlewares
283
+ const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
284
+ // Check if route is simple (no middleware, validation, or static serve)
285
+ const isSimpleRoute = applicableGlobalMiddlewares.length === 0 &&
286
+ (!route.middlewares || route.middlewares.length === 0) &&
287
+ !route.validator &&
288
+ !route.staticServe;
289
+ if (isSimpleRoute) {
290
+ // Use fast path handler for simple routes (minimal overhead)
291
+ routes[path][method] = this.createFastPathHandler(route);
292
+ }
293
+ else {
294
+ // Use full-featured handler for complex routes
295
+ routes[path][method] = this.createRouteHandler(route, commonMiddlewares);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ // Add 404 handler with static headers
301
+ routes['/*'] = () => {
302
+ return new Response(JSON.stringify({ error: 'Not Found' }), {
303
+ status: 404,
304
+ headers: STATIC_JSON_HEADERS,
305
+ });
306
+ };
307
+ return routes;
308
+ }
309
+ /**
310
+ * Builds WebSocket routes from queued WebSocket route registrations
311
+ *
312
+ * Converts Asena WebSocket route format to Bun's native router format
313
+ * with middleware chain support. Each WebSocket route creates a GET
314
+ * handler that executes middlewares before attempting WebSocket upgrade.
315
+ *
316
+ * @returns Bun router object for WebSocket routes
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * // Returns:
321
+ * {
322
+ * "/chat": {
323
+ * GET: (req) => Response | undefined // upgrade handler with middlewares
324
+ * }
325
+ * }
326
+ * ```
327
+ */
328
+ buildWebSocketRoutes() {
329
+ const routes = {};
330
+ for (const wsRoute of this.wsRouteQueue) {
331
+ // Normalize path - ensure it starts with /
332
+ const path = wsRoute.path.startsWith('/') ? wsRoute.path : `/${wsRoute.path}`;
333
+ // Initialize path object
334
+ routes[path] = routes[path] || {};
335
+ // Add GET handler for WebSocket upgrade (with middleware chain)
336
+ routes[path]['GET'] = this.createWebSocketUpgradeHandler(wsRoute);
337
+ }
338
+ return routes;
339
+ }
340
+ /**
341
+ * Executes middleware chain with recursive next() pattern
342
+ *
343
+ * Middlewares can control execution flow by:
344
+ * - Calling await next() to proceed to next middleware
345
+ * - Returning false to stop pipeline (403 response)
346
+ * - Returning Response to send custom response and stop pipeline
347
+ * - Throwing HttpException to send custom HTTP error response
348
+ * - Throwing error to trigger error handler
349
+ *
350
+ * @param context - Request context
351
+ * @param middlewares - Array of middlewares to execute
352
+ * @param index - Current middleware index (internal)
353
+ * @returns Promise<boolean | Response> - true to continue, false to stop with 403, Response to send custom response
354
+ *
355
+ * @example
356
+ * ```typescript
357
+ * const result = await this.executeMiddlewares(context, globalMiddlewares);
358
+ * if (result === false) {
359
+ * return new Response('Forbidden', { status: 403 });
360
+ * }
361
+ * if (result instanceof Response) {
362
+ * return result;
363
+ * }
364
+ * ```
365
+ */
366
+ async executeMiddlewares(context, middlewares, index = 0) {
367
+ // Base case: all middlewares executed successfully
368
+ if (index >= middlewares.length) {
369
+ return true;
370
+ }
371
+ const middleware = middlewares[index];
372
+ // Create next() function that executes the next middleware in chain
373
+ const next = async () => {
374
+ const result = await this.executeMiddlewares(context, middlewares, index + 1);
375
+ // If next middleware returned Response, throw MiddlewareResponseError to propagate it
376
+ if (result instanceof Response) {
377
+ throw new MiddlewareResponseError(result);
378
+ }
379
+ // If next middleware returned false, throw to stop current middleware
380
+ if (result === false) {
381
+ throw new Error('MIDDLEWARE_CHAIN_STOPPED');
382
+ }
383
+ };
384
+ try {
385
+ // Execute middleware with real next() function
386
+ const result = await middleware.handle(context, next);
387
+ // If middleware returns Response, stop pipeline and return response
388
+ if (result instanceof Response) {
389
+ return result;
390
+ }
391
+ // If middleware returns false, stop pipeline
392
+ if (result === false) {
393
+ return false;
394
+ }
395
+ // Middleware executed successfully
396
+ return true;
397
+ }
398
+ catch (error) {
399
+ // If middleware threw HttpException, convert to Response
400
+ if (error instanceof HttpException) {
401
+ return error.getResponse();
402
+ }
403
+ // If a downstream middleware returned a Response, propagate it
404
+ if (error instanceof MiddlewareResponseError) {
405
+ return error.response;
406
+ }
407
+ // If a downstream middleware stopped the chain with false, propagate the stop
408
+ if (error instanceof Error && error.message === 'MIDDLEWARE_CHAIN_STOPPED') {
409
+ return false;
410
+ }
411
+ // Other errors should be thrown (will be caught by route handler)
412
+ throw error;
413
+ }
414
+ }
415
+ /**
416
+ * Creates a WebSocket upgrade handler with middleware chain support
417
+ *
418
+ * The handler executes in the following order:
419
+ * 1. Create context wrapper
420
+ * 2. Execute global middlewares (pattern-filtered, if any return false, reject with 403)
421
+ * 3. Execute route-specific middlewares (if any return false, reject with 403)
422
+ * 4. Attempt WebSocket upgrade via server.upgrade()
423
+ * 5. Return undefined if upgrade successful, error response otherwise
424
+ *
425
+ * @param wsRoute - WebSocket route parameters
426
+ * @returns Bun-compatible WebSocket upgrade handler
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const handler = createWebSocketUpgradeHandler({
431
+ * path: '/chat',
432
+ * middlewares: [authMiddleware],
433
+ * websocketService: chatService
434
+ * });
435
+ * // handler(req) => Response | undefined
436
+ * ```
437
+ */
438
+ createWebSocketUpgradeHandler(wsRoute) {
439
+ // ✅ Filter global middlewares by path pattern (ONCE during route building)
440
+ // This happens at server startup, NOT on every request → zero runtime overhead
441
+ const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(wsRoute.path);
442
+ return async (req) => {
443
+ try {
444
+ // Create context wrapper
445
+ const context = new ErgenecoreContextWrapper(req);
446
+ // Execute filtered global middlewares with real next() chain
447
+ if (applicableGlobalMiddlewares.length > 0) {
448
+ const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
449
+ // If middleware returned a custom response, return it
450
+ if (result instanceof Response) {
451
+ return result;
452
+ }
453
+ // If middleware returned false, return 403
454
+ if (result === false) {
455
+ return new Response('Forbidden', { status: 403 });
456
+ }
457
+ }
458
+ // Execute route-specific middlewares with real next() chain
459
+ if (wsRoute.middlewares && wsRoute.middlewares.length > 0) {
460
+ const result = await this.executeMiddlewares(context, wsRoute.middlewares);
461
+ // If middleware returned a custom response, return it
462
+ if (result instanceof Response) {
463
+ return result;
464
+ }
465
+ // If middleware returned false, return 403
466
+ if (result === false) {
467
+ return new Response('Forbidden', { status: 403 });
468
+ }
469
+ }
470
+ // Attempt WebSocket upgrade
471
+ // Use wsRoute.path (actual route path) instead of namespace (which might be the service name)
472
+ const upgraded = this.server.upgrade(req, {
473
+ data: {
474
+ path: wsRoute.path,
475
+ id: `conn-${Date.now()}-${Math.random().toString(36).substring(7)}`,
476
+ values: context.getWebSocketValue(),
477
+ },
478
+ });
479
+ if (upgraded) {
480
+ return undefined; // Upgrade successful
481
+ }
482
+ // Upgrade failed
483
+ return new Response('WebSocket upgrade failed', { status: 500 });
484
+ }
485
+ catch (error) {
486
+ // If HttpException was thrown, convert to Response
487
+ if (error instanceof HttpException) {
488
+ return error.getResponse();
489
+ }
490
+ // Log and handle other errors
491
+ this.logger.error('WebSocket upgrade handler error:', error);
492
+ return new Response(JSON.stringify({
493
+ error: error instanceof Error ? error.message : 'WebSocket upgrade failed',
494
+ }), {
495
+ status: 500,
496
+ headers: { 'Content-Type': 'application/json' },
497
+ });
498
+ }
499
+ };
500
+ }
501
+ /**
502
+ * Checks for path collisions between HTTP and WebSocket routes
503
+ *
504
+ * Validates that no HTTP GET route conflicts with a WebSocket route
505
+ * on the same path, as both use GET method for their handlers.
506
+ *
507
+ * @param httpRoutes - HTTP routes object from buildBunRoutes()
508
+ * @param wsRoutes - WebSocket routes object from buildWebSocketRoutes()
509
+ * @throws Error if collision detected
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * checkPathCollisions(
514
+ * { "/api/data": { GET: handler1 } },
515
+ * { "/api/data": { GET: wsHandler } }
516
+ * );
517
+ * // Throws: Route collision detected at path "/api/data": Both HTTP and WebSocket routes define GET method
518
+ * ```
519
+ */
520
+ checkPathCollisions(httpRoutes, wsRoutes) {
521
+ for (const wsPath of Object.keys(wsRoutes)) {
522
+ // Check if HTTP routes have the same path
523
+ if (httpRoutes[wsPath]) {
524
+ // Check if HTTP route has GET method (collision with WebSocket GET)
525
+ if (httpRoutes[wsPath]['GET']) {
526
+ throw new Error(`Route collision detected at path "${wsPath}": Both HTTP and WebSocket routes define GET method. ` +
527
+ `WebSocket routes use GET for upgrade handshake, so HTTP GET cannot be registered on the same path.`);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ /**
533
+ * Merges HTTP and WebSocket routes into a single router object
534
+ *
535
+ * Combines routes from buildBunRoutes() and buildWebSocketRoutes(),
536
+ * ensuring that methods on the same path are properly merged.
537
+ *
538
+ * @param httpRoutes - HTTP routes object
539
+ * @param wsRoutes - WebSocket routes object
540
+ * @returns Merged routes object
541
+ *
542
+ * @example
543
+ * ```typescript
544
+ * mergeRoutes(
545
+ * { "/api": { POST: handler1, PUT: handler2 } },
546
+ * { "/api": { GET: wsHandler } }
547
+ * );
548
+ * // Returns: { "/api": { POST: handler1, PUT: handler2, GET: wsHandler } }
549
+ * ```
550
+ */
551
+ mergeRoutes(httpRoutes, wsRoutes) {
552
+ const merged = { ...httpRoutes };
553
+ for (const [path, methods] of Object.entries(wsRoutes)) {
554
+ if (merged[path]) {
555
+ // Merge methods for existing path
556
+ merged[path] = { ...merged[path], ...methods };
557
+ }
558
+ else {
559
+ // Add new path
560
+ merged[path] = methods;
561
+ }
562
+ }
563
+ return merged;
564
+ }
565
+ /**
566
+ * Creates a fast path handler for simple routes
567
+ *
568
+ * This handler is optimized for routes without:
569
+ * - Middlewares (global or route-level)
570
+ * - Validators
571
+ * - Static file serving
572
+ *
573
+ * Benefits:
574
+ * - Minimal object allocation
575
+ * - No conditional checks
576
+ * - Direct handler execution
577
+ * - Reduced try-catch overhead
578
+ *
579
+ * @param route - Route parameters
580
+ * @returns Bun-compatible fast path handler
581
+ */
582
+ createFastPathHandler(route) {
583
+ // If no error handler is set, use ultra-minimal version with default error handling
584
+ if (!this.errorHandler) {
585
+ return async (req) => {
586
+ const context = new ErgenecoreContextWrapper(req);
587
+ try {
588
+ // Inject Bun's native route params if present
589
+ // @ts-ignore - Bun adds params to Request
590
+ if (req.params) {
591
+ // @ts-ignore - Bun adds params to Request
592
+ const params = req.params;
593
+ // eslint-disable-next-line guard-for-in
594
+ for (const key in params) {
595
+ context.setValue(`param:${key}`, params[key]);
596
+ }
597
+ }
598
+ // Execute handler directly
599
+ const response = await route.handler(context);
600
+ // Return response (check if already a Response object)
601
+ if (response instanceof Response) {
602
+ return response;
603
+ }
604
+ // Wrap in Response with static headers
605
+ return new Response(JSON.stringify(response), {
606
+ status: 200,
607
+ headers: STATIC_JSON_HEADERS,
608
+ });
609
+ }
610
+ catch (error) {
611
+ // If handler threw HttpException, convert to Response
612
+ if (error instanceof HttpException) {
613
+ return error.getResponse();
614
+ }
615
+ // Default error handling when no custom error handler is registered
616
+ this.logger.error('Route handler error:', error);
617
+ return new Response(JSON.stringify({
618
+ error: error instanceof Error ? error.message : 'Internal Server Error',
619
+ }), {
620
+ status: 500,
621
+ headers: STATIC_JSON_HEADERS,
622
+ });
623
+ }
624
+ };
625
+ }
626
+ // With error handler (minimal try-catch)
627
+ return async (req) => {
628
+ const context = new ErgenecoreContextWrapper(req);
629
+ try {
630
+ // Inject Bun's native route params if present
631
+ // @ts-ignore - Bun adds params to Request
632
+ if (req.params) {
633
+ // @ts-ignore - Bun adds params to Request
634
+ const params = req.params;
635
+ // eslint-disable-next-line guard-for-in
636
+ for (const key in params) {
637
+ context.setValue(`param:${key}`, params[key]);
638
+ }
639
+ }
640
+ // Execute handler directly
641
+ const response = await route.handler(context);
642
+ // Return response (check if already a Response object)
643
+ if (response instanceof Response) {
644
+ return response;
645
+ }
646
+ // Wrap in Response with static headers
647
+ return new Response(JSON.stringify(response), {
648
+ status: 200,
649
+ headers: STATIC_JSON_HEADERS,
650
+ });
651
+ }
652
+ catch (error) {
653
+ // If handler threw HttpException, convert to Response
654
+ if (error instanceof HttpException) {
655
+ return error.getResponse();
656
+ }
657
+ // Handle other errors with custom error handler
658
+ return this.errorHandler(error, context);
659
+ }
660
+ };
661
+ }
662
+ /**
663
+ * Creates a route handler function for Bun's router
664
+ *
665
+ * Wraps the Asena handler with:
666
+ * - CoreContextWrapper for context abstraction
667
+ * - Parameter injection from Bun's native parser
668
+ * - Global and route middlewares (pattern-filtered)
669
+ * - Error handling
670
+ *
671
+ * @param route - Route parameters
672
+ * @param _commonMiddlewares - Common middlewares for this route group (for future optimization)
673
+ * @returns Bun-compatible route handler
674
+ */
675
+ createRouteHandler(route, _commonMiddlewares = []) {
676
+ // ✅ Filter global middlewares by path pattern (ONCE during route building)
677
+ // This happens at server startup, NOT on every request → zero runtime overhead
678
+ const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
679
+ return async (req) => {
680
+ // Create context wrapper outside try block so it's accessible in catch
681
+ const context = new ErgenecoreContextWrapper(req);
682
+ // Inject Bun's native route params
683
+ // @ts-ignore - Bun adds params to Request
684
+ if (req.params) {
685
+ // @ts-ignore - Bun adds params to Request
686
+ const params = req.params;
687
+ // eslint-disable-next-line guard-for-in
688
+ for (const key in params) {
689
+ context.setValue(`param:${key}`, params[key]);
690
+ }
691
+ }
692
+ try {
693
+ // Execute filtered global middlewares with real next() chain
694
+ if (applicableGlobalMiddlewares.length > 0) {
695
+ const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
696
+ // If middleware returned a custom response, return it
697
+ if (result instanceof Response) {
698
+ return result;
699
+ }
700
+ // If middleware returned false, return 403
701
+ if (result === false) {
702
+ return new Response('Forbidden', { status: 403 });
703
+ }
704
+ }
705
+ // Execute route middlewares with real next() chain
706
+ if (route.middlewares && route.middlewares.length > 0) {
707
+ const result = await this.executeMiddlewares(context, route.middlewares);
708
+ // If middleware returned a custom response, return it
709
+ if (result instanceof Response) {
710
+ return result;
711
+ }
712
+ // If middleware returned false, return 403
713
+ if (result === false) {
714
+ return new Response('Forbidden', { status: 403 });
715
+ }
716
+ }
717
+ // Execute validation
718
+ if (route.validator) {
719
+ const validationResult = await this.validateRequest(context, route.validator);
720
+ if (validationResult)
721
+ return validationResult;
722
+ }
723
+ // Handle static file serving
724
+ if (route.staticServe) {
725
+ const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
726
+ if (staticResponse)
727
+ return staticResponse;
728
+ }
729
+ // Execute route handler
730
+ const response = await route.handler(context);
731
+ // If handler returns Response, return it directly
732
+ if (response instanceof Response) {
733
+ return response;
734
+ }
735
+ // Otherwise, wrap in Response
736
+ return new Response(JSON.stringify(response), {
737
+ status: 200,
738
+ headers: { 'Content-Type': 'application/json' },
739
+ });
740
+ }
741
+ catch (error) {
742
+ // If handler or middleware threw HttpException, convert to Response
743
+ if (error instanceof HttpException) {
744
+ return error.getResponse();
745
+ }
746
+ // Handle other errors with custom error handler if available
747
+ if (this.errorHandler) {
748
+ // Pass the original context with params already injected
749
+ return await this.errorHandler(error, context);
750
+ }
751
+ // Default error response
752
+ this.logger.error('Route handler error:', error);
753
+ return new Response(JSON.stringify({
754
+ error: error instanceof Error ? error.message : 'Internal Server Error',
755
+ }), {
756
+ status: 500,
757
+ headers: { 'Content-Type': 'application/json' },
758
+ });
759
+ }
760
+ };
761
+ }
762
+ /**
763
+ * Type guard to check if validation schema has hook format
764
+ *
765
+ * Discriminates between ValidationSchemaWithHook and plain ValidationSchema.
766
+ * Uses runtime check for 'schema' property to determine type.
767
+ *
768
+ * @param schema - Validation schema to check
769
+ * @returns true if schema is ValidationSchemaWithHook format
770
+ *
771
+ * @example
772
+ * ```typescript
773
+ * if (isValidationSchemaWithHook(schema)) {
774
+ * // schema.schema and schema.hook are available
775
+ * } else {
776
+ * // schema is a plain Zod schema
777
+ * }
778
+ * ```
779
+ */
780
+ isValidationSchemaWithHook(schema) {
781
+ return typeof schema === 'object' && schema !== null && 'schema' in schema;
782
+ }
783
+ /**
784
+ * Validates request data using Zod schemas
785
+ *
786
+ * Checks each validation target (body, query, param, header) and runs
787
+ * the Zod schema validation. If validation fails and a hook is provided,
788
+ * the hook is called to generate a custom error response.
789
+ *
790
+ * Supports two validation formats:
791
+ * 1. Plain Zod schema: z.object({...})
792
+ * 2. Schema with hook: { schema: z.object({...}), hook?: (...) => {...} }
793
+ *
794
+ * @param context - Request context
795
+ * @param validator - Validator instance with schema definitions
796
+ * @returns Response if validation fails, null if passes
797
+ */
798
+ async validateRequest(context, validator) {
799
+ // Iterate through all validator methods (body, query, param, header)
800
+ for (const key of VALIDATOR_METHODS) {
801
+ const validatorHandler = validator[key];
802
+ // Skip if validator not defined for this target
803
+ if (!validatorHandler || typeof validatorHandler.handle !== 'function') {
804
+ continue;
805
+ }
806
+ // Get validation schema
807
+ const validationSchema = await validatorHandler.handle();
808
+ // Skip if no validation schema provided
809
+ if (!validationSchema) {
810
+ continue;
811
+ }
812
+ // Use type guard to discriminate between formats
813
+ const schema = this.isValidationSchemaWithHook(validationSchema)
814
+ ? validationSchema.schema
815
+ : validationSchema;
816
+ const hook = this.isValidationSchemaWithHook(validationSchema) ? (validationSchema.hook ?? null) : null;
817
+ // Extract data to validate
818
+ const data = await this.extractValidationData(context, key);
819
+ // Run Zod validation
820
+ const result = schema.safeParse(data);
821
+ // If validation fails
822
+ if (!result.success) {
823
+ // Use custom hook if provided
824
+ if (hook) {
825
+ const hookResponse = await hook(result, context);
826
+ if (hookResponse)
827
+ return hookResponse;
828
+ }
829
+ // Default error response
830
+ return new Response(JSON.stringify({
831
+ error: 'Validation failed',
832
+ details: result.error.flatten(),
833
+ }), {
834
+ status: 400,
835
+ headers: { 'Content-Type': 'application/json' },
836
+ });
837
+ }
838
+ }
839
+ // All validations passed
840
+ return null;
841
+ }
842
+ /**
843
+ * Extracts data from request based on validation target
844
+ *
845
+ * @param context - Request context
846
+ * @param target - Validation target (body, query, param, header)
847
+ * @returns Data to be validated
848
+ */
849
+ async extractValidationData(context, target) {
850
+ switch (target) {
851
+ case 'json':
852
+ return await context.getBody();
853
+ case 'body': {
854
+ // 'json' is the correct Asena validator method name
855
+ // 'body' is kept for backwards compatibility
856
+ return await context.getBody();
857
+ }
858
+ case 'query': {
859
+ const url = new URL(context.req.url);
860
+ const queryObj = {};
861
+ url.searchParams.forEach((value, key) => {
862
+ queryObj[key] = value;
863
+ });
864
+ return queryObj;
865
+ }
866
+ case 'param': {
867
+ // Extract all params from context
868
+ const params = {};
869
+ // We stored params with 'param:' prefix in context
870
+ for (const [key, value] of context.values.entries()) {
871
+ if (key.startsWith('param:')) {
872
+ params[key.replace('param:', '')] = value;
873
+ }
874
+ }
875
+ return params;
876
+ }
877
+ case 'header': {
878
+ const headers = {};
879
+ context.req.headers.forEach((value, key) => {
880
+ headers[key.toLowerCase()] = value;
881
+ });
882
+ return headers;
883
+ }
884
+ default:
885
+ return {};
886
+ }
887
+ }
888
+ /**
889
+ * Serves static files using Bun.file() API
890
+ *
891
+ * This method handles static file serving with the following features:
892
+ * - Path rewriting via rewriteRequestPath
893
+ * - Root directory resolution
894
+ * - File existence checking
895
+ * - onFound/onNotFound hooks
896
+ * - Automatic MIME type detection (via Bun.file())
897
+ *
898
+ * @param req - Native Bun Request object
899
+ * @param context - ErgenecoreContextWrapper instance
900
+ * @param staticServe - Static serve configuration from route
901
+ * @returns Response object if file found/not found, null if hooks override
902
+ *
903
+ * @example
904
+ * ```typescript
905
+ * const response = await this.serveStaticFile(req, context, {
906
+ * root: '/public',
907
+ * rewriteRequestPath: (path) => path.replace('/static', ''),
908
+ * onFound: { handler: async () => {}, override: false },
909
+ * onNotFound: { handler: async () => {}, override: false }
910
+ * });
911
+ * ```
912
+ */
913
+ async serveStaticFile(req, context, staticServe) {
914
+ try {
915
+ // 1. Extract request path from URL
916
+ const url = new URL(req.url);
917
+ const requestPath = decodeURIComponent(url.pathname); // Decode URL encoding
918
+ // 2. Apply path rewriting if provided
919
+ const rewrittenPath = staticServe.rewriteRequestPath ? staticServe.rewriteRequestPath(requestPath) : requestPath;
920
+ // 3. Build absolute file path
921
+ const filePath = path.join(staticServe.root, rewrittenPath);
922
+ // 4. Security: Resolve and validate path to prevent traversal attacks
923
+ // Resolve both paths to absolute canonical paths
924
+ const resolvedFilePath = path.resolve(filePath);
925
+ const resolvedRoot = path.resolve(staticServe.root);
926
+ // Check if resolved file path is within root directory
927
+ if (!resolvedFilePath.startsWith(resolvedRoot)) {
928
+ this.logger.warn(`Path traversal attempt detected: ${requestPath} -> ${resolvedFilePath}`);
929
+ return new Response('Forbidden', { status: 403 });
930
+ }
931
+ // 5. Create Bun.file() instance
932
+ const file = Bun.file(resolvedFilePath);
933
+ // 5. Check if file exists
934
+ const fileExists = await file.exists();
935
+ // 6. File not found → trigger onNotFound hook
936
+ if (!fileExists) {
937
+ if (staticServe.onNotFound) {
938
+ await staticServe.onNotFound.handler(rewrittenPath, context);
939
+ // If hook overrides, return null (let handler continue)
940
+ if (staticServe.onNotFound.override) {
941
+ return null;
942
+ }
943
+ }
944
+ // Default 404 response
945
+ return new Response('Not Found', { status: 404 });
946
+ }
947
+ // 7. File found → trigger onFound hook
948
+ if (staticServe.onFound) {
949
+ await staticServe.onFound.handler(rewrittenPath, context);
950
+ // If hook overrides, return null (let handler continue)
951
+ if (staticServe.onFound.override) {
952
+ return null;
953
+ }
954
+ }
955
+ // 8. Create Bun file response with automatic Content-Type detection
956
+ const fileResponse = new Response(file);
957
+ // 9. Build response headers starting with Bun's Content-Type
958
+ const finalHeaders = new Headers(fileResponse.headers);
959
+ // 10. Add cache validation headers (ETag and Last-Modified)
960
+ // ETag format: W/"<size>-<lastModified>" (weak validator)
961
+ const etag = `W/"${file.size}-${file.lastModified}"`;
962
+ finalHeaders.set('ETag', etag);
963
+ // Last-Modified: HTTP date format (RFC 7231)
964
+ const lastModified = new Date(file.lastModified).toUTCString();
965
+ finalHeaders.set('Last-Modified', lastModified);
966
+ // 11. Add custom headers (can override defaults)
967
+ const customHeaders = this.buildStaticFileHeaders(resolvedFilePath, staticServe.extra);
968
+ for (const [key, value] of Object.entries(customHeaders)) {
969
+ finalHeaders.set(key, value);
970
+ }
971
+ // 12. Add default Cache-Control if not provided
972
+ // Using 'public, max-age=0' allows caching but requires revalidation
973
+ if (!finalHeaders.has('Cache-Control')) {
974
+ finalHeaders.set('Cache-Control', 'public, max-age=0');
975
+ }
976
+ // 13. Return response with all headers
977
+ // This preserves Bun's zero-copy file serving while adding cache headers
978
+ return new Response(fileResponse.body, {
979
+ status: fileResponse.status,
980
+ headers: finalHeaders,
981
+ });
982
+ }
983
+ catch (error) {
984
+ this.logger.error('Static file serving error:', error);
985
+ return new Response(JSON.stringify({
986
+ error: error instanceof Error ? error.message : 'Static file serving failed',
987
+ }), {
988
+ status: 500,
989
+ headers: { 'Content-Type': 'application/json' },
990
+ });
991
+ }
992
+ }
993
+ /**
994
+ * Builds response headers for static file serving
995
+ *
996
+ * Constructs headers with:
997
+ * - Content-Type (custom MIME or Bun default)
998
+ * - Cache-Control (if specified)
999
+ * - Custom headers (if specified)
1000
+ *
1001
+ * @param filePath - Resolved absolute file path
1002
+ * @param extras - StaticServeExtras configuration
1003
+ * @returns Headers object for Response
1004
+ *
1005
+ * @example
1006
+ * ```typescript
1007
+ * const headers = this.buildStaticFileHeaders('/app/public/style.css', {
1008
+ * cacheControl: 'public, max-age=86400',
1009
+ * headers: { 'X-Custom': 'value' },
1010
+ * mimes: { '.css': 'text/css; charset=utf-8' }
1011
+ * });
1012
+ * // Returns: { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': '...', 'X-Custom': 'value' }
1013
+ * ```
1014
+ */
1015
+ buildStaticFileHeaders(filePath, extras) {
1016
+ const headers = {};
1017
+ if (!extras) {
1018
+ return headers;
1019
+ }
1020
+ // 1. Content-Type: Check custom MIME types first
1021
+ if (extras.mimes) {
1022
+ const ext = path.extname(filePath);
1023
+ const customMime = extras.mimes[ext];
1024
+ if (customMime) {
1025
+ headers['Content-Type'] = customMime;
1026
+ }
1027
+ }
1028
+ // Note: If no custom MIME, Bun.file() will set Content-Type automatically
1029
+ // 2. Cache-Control header
1030
+ if (extras.cacheControl) {
1031
+ headers['Cache-Control'] = extras.cacheControl;
1032
+ }
1033
+ // 3. Custom headers
1034
+ if (extras.headers) {
1035
+ for (const [key, value] of Object.entries(extras.headers)) {
1036
+ headers[key] = value;
1037
+ }
1038
+ }
1039
+ return headers;
1040
+ }
1041
+ /**
1042
+ * Extracts base path from a route path
1043
+ *
1044
+ * Removes dynamic segments (parameters and wildcards) from the path
1045
+ * to find the static base path for grouping routes.
1046
+ *
1047
+ * @param path - Route path (e.g., "/api/users/:id")
1048
+ * @returns Base path without dynamic segments (e.g., "/api/users")
1049
+ *
1050
+ * @example
1051
+ * ```typescript
1052
+ * extractBasePath('/api/users/:id') // => '/api/users'
1053
+ * extractBasePath('/api/users/:id/posts/:postId') // => '/api/users'
1054
+ * extractBasePath('/static/*') // => '/static'
1055
+ * extractBasePath('/') // => '/'
1056
+ * ```
1057
+ */
1058
+ extractBasePath(path) {
1059
+ // Remove trailing slash (except for root)
1060
+ let normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
1061
+ // Split path into segments
1062
+ const segments = normalized.split('/');
1063
+ const baseSegments = [];
1064
+ // Keep segments until we hit a parameter or wildcard
1065
+ for (const segment of segments) {
1066
+ // Stop if segment is a parameter (:param) or wildcard (*)
1067
+ if (segment.startsWith(':') || segment.includes('*')) {
1068
+ break;
1069
+ }
1070
+ baseSegments.push(segment);
1071
+ }
1072
+ // Join back into path, handle root case
1073
+ return baseSegments.join('/') || '/';
1074
+ }
1075
+ /**
1076
+ * Extracts common middlewares across multiple routes
1077
+ *
1078
+ * Identifies middlewares that are present in ALL routes and can be
1079
+ * moved to group level for optimization.
1080
+ *
1081
+ * @param routes - Array of routes to analyze
1082
+ * @returns Array of common middleware instances
1083
+ *
1084
+ * @example
1085
+ * ```typescript
1086
+ * extractCommonMiddlewares([
1087
+ * { middlewares: [auth, log, rate] },
1088
+ * { middlewares: [auth, log] },
1089
+ * { middlewares: [auth, log] }
1090
+ * ]) // => [auth, log]
1091
+ * ```
1092
+ */
1093
+ extractCommonMiddlewares(routes) {
1094
+ // Need at least 2 routes to have common middlewares
1095
+ if (routes.length === 0 || routes.length === 1) {
1096
+ return [];
1097
+ }
1098
+ // Get middlewares from first route as baseline
1099
+ const firstRouteMiddlewares = routes[0].middlewares || [];
1100
+ // Filter to only middlewares present in ALL routes
1101
+ return firstRouteMiddlewares.filter((middleware) => {
1102
+ return routes.every((route) => {
1103
+ return (route.middlewares || []).some((mw) => {
1104
+ // Compare by constructor name (class identity)
1105
+ return mw.constructor.name === middleware.constructor.name;
1106
+ });
1107
+ });
1108
+ });
1109
+ }
1110
+ /**
1111
+ * Groups routes by their base path
1112
+ *
1113
+ * Creates a map of base paths to routes for optimization.
1114
+ * Routes with the same base path will share common middlewares.
1115
+ *
1116
+ * @param routes - Array of routes to group
1117
+ * @returns Map of base paths to route arrays
1118
+ *
1119
+ * @example
1120
+ * ```typescript
1121
+ * groupRoutesByBasePath([
1122
+ * { path: '/api/users' },
1123
+ * { path: '/api/users/:id' },
1124
+ * { path: '/api/posts' }
1125
+ * ])
1126
+ * // => Map {
1127
+ * // '/api/users' => [route1, route2],
1128
+ * // '/api/posts' => [route3]
1129
+ * // }
1130
+ * ```
1131
+ */
1132
+ groupRoutesByBasePath(routes) {
1133
+ const groups = new Map();
1134
+ for (const route of routes) {
1135
+ const basePath = this.extractBasePath(route.path);
1136
+ if (!groups.has(basePath)) {
1137
+ groups.set(basePath, []);
1138
+ }
1139
+ groups.get(basePath).push(route);
1140
+ }
1141
+ return groups;
1142
+ }
1143
+ get hostname() {
1144
+ return this._hostname;
1145
+ }
1146
+ set hostname(value) {
1147
+ this._hostname = value;
1148
+ }
1149
+ /**
1150
+ * Groups HTTP routes by controller name
1151
+ *
1152
+ * Creates a map of controller names to their routes for organized logging.
1153
+ * Each group contains the controller's base path and all its routes.
1154
+ *
1155
+ * @returns Map of controller names to route groups
1156
+ *
1157
+ * @example
1158
+ * ```typescript
1159
+ * groupRoutesByController()
1160
+ * // => Map {
1161
+ * // 'UserController' => {
1162
+ * // basePath: '/users',
1163
+ * // routes: [{ method: 'GET', path: '/users' }, { method: 'POST', path: '/users' }]
1164
+ * // }
1165
+ * // }
1166
+ * ```
1167
+ */
1168
+ groupRoutesByController() {
1169
+ const groups = new Map();
1170
+ for (const route of this.routeQueue) {
1171
+ const controllerName = route.controllerName || 'Unknown';
1172
+ const controllerBasePath = route.controllerBasePath || '/';
1173
+ if (!groups.has(controllerName)) {
1174
+ groups.set(controllerName, {
1175
+ basePath: controllerBasePath,
1176
+ routes: [],
1177
+ });
1178
+ }
1179
+ groups.get(controllerName).routes.push({
1180
+ method: route.method.toUpperCase(),
1181
+ path: route.path,
1182
+ });
1183
+ }
1184
+ return groups;
1185
+ }
1186
+ /**
1187
+ * Groups WebSocket routes by controller name
1188
+ *
1189
+ * Creates a map of controller names to their WebSocket routes for organized logging.
1190
+ *
1191
+ * @returns Map of controller names to WebSocket route groups
1192
+ *
1193
+ * @example
1194
+ * ```typescript
1195
+ * groupWebSocketRoutesByController()
1196
+ * // => Map {
1197
+ * // 'ChatController' => {
1198
+ * // basePath: '/chat',
1199
+ * // routes: [{ path: '/chat' }]
1200
+ * // }
1201
+ * // }
1202
+ * ```
1203
+ */
1204
+ groupWebSocketRoutesByController() {
1205
+ const groups = new Map();
1206
+ for (const wsRoute of this.wsRouteQueue) {
1207
+ const controllerName = wsRoute.controllerName || 'Unknown';
1208
+ const controllerBasePath = wsRoute.path; // WebSocket uses path as base path
1209
+ if (!groups.has(controllerName)) {
1210
+ groups.set(controllerName, {
1211
+ basePath: controllerBasePath,
1212
+ routes: [],
1213
+ });
1214
+ }
1215
+ groups.get(controllerName).routes.push({
1216
+ path: wsRoute.path,
1217
+ });
1218
+ }
1219
+ return groups;
1220
+ }
1221
+ /**
1222
+ * Filters global middlewares for a specific route path
1223
+ *
1224
+ * Uses pattern matching to determine which middlewares should apply to this path.
1225
+ * Pattern matching happens ONCE during route building (deferred registration),
1226
+ * not on every request, ensuring zero runtime overhead.
1227
+ *
1228
+ * @param path - Route path (e.g., '/api/users', '/users/:id')
1229
+ * @returns Array of middlewares that should apply to this path
1230
+ *
1231
+ * @example
1232
+ * ```typescript
1233
+ * // Given:
1234
+ * // - LoggerMiddleware (no config → applies to all routes)
1235
+ * // - AuthMiddleware (include: ['/api/*'])
1236
+ * // - RateLimitMiddleware (exclude: ['/health'])
1237
+ *
1238
+ * getGlobalMiddlewaresForPath('/api/users')
1239
+ * // => [LoggerMiddleware, AuthMiddleware, RateLimitMiddleware]
1240
+ *
1241
+ * getGlobalMiddlewaresForPath('/health')
1242
+ * // => [LoggerMiddleware, AuthMiddleware] (RateLimit excluded)
1243
+ *
1244
+ * getGlobalMiddlewaresForPath('/public/page')
1245
+ * // => [LoggerMiddleware, RateLimitMiddleware] (Auth not included)
1246
+ * ```
1247
+ */
1248
+ getGlobalMiddlewaresForPath(path) {
1249
+ return this.globalMiddlewares
1250
+ .filter(({ config }) => shouldApplyMiddleware(path, config))
1251
+ .map(({ middleware }) => middleware);
1252
+ }
1253
+ /**
1254
+ * Logs a summary of registered controllers
1255
+ *
1256
+ * Displays colored success messages for each controller with route counts.
1257
+ * HTTP controllers show total HTTP routes, WebSocket-only controllers are
1258
+ * displayed separately.
1259
+ *
1260
+ * @example
1261
+ * Output format:
1262
+ * ```
1263
+ * ✓ Successfully registered CONTROLLER UserController (2 routes)
1264
+ * ✓ Successfully registered WEBSOCKET ChatController (1 route)
1265
+ * ```
1266
+ */
1267
+ logControllerSummary() {
1268
+ const httpGroups = this.groupRoutesByController();
1269
+ const wsGroups = this.groupWebSocketRoutesByController();
1270
+ // Log HTTP controllers
1271
+ for (const [controllerName, group] of httpGroups) {
1272
+ const routeCount = group.routes.length;
1273
+ const routeText = routeCount === 1 ? 'route' : 'routes';
1274
+ this.logger.info(`${green('✓')} Successfully registered ${yellow('CONTROLLER')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
1275
+ }
1276
+ // Log WebSocket controllers (only those that don't have HTTP routes)
1277
+ for (const [controllerName, group] of wsGroups) {
1278
+ if (!httpGroups.has(controllerName)) {
1279
+ const routeCount = group.routes.length;
1280
+ const routeText = routeCount === 1 ? 'route' : 'routes';
1281
+ this.logger.info(`${green('✓')} Successfully registered ${yellow('WEBSOCKET')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
1282
+ }
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Builds controller-based log output with colors
1287
+ *
1288
+ * Creates a formatted string showing all routes grouped by controller,
1289
+ * with HTTP controllers listed first, then WebSocket namespaces separately.
1290
+ *
1291
+ * Color scheme:
1292
+ * - Controller name: blue
1293
+ * - Base path: yellow
1294
+ * - GET method: green
1295
+ * - POST method: blue
1296
+ * - PUT method: yellow
1297
+ * - DELETE method: red
1298
+ * - WS method: blue
1299
+ *
1300
+ * @returns Formatted log string with color codes
1301
+ *
1302
+ * @example
1303
+ * Output format:
1304
+ * ```
1305
+ * UserController (/users):
1306
+ * GET /users
1307
+ * GET /users/:id
1308
+ * POST /users
1309
+ *
1310
+ * ChatNamespace (chat):
1311
+ * WS /chat
1312
+ * ```
1313
+ */
1314
+ buildControllerBasedLog() {
1315
+ const httpGroups = this.groupRoutesByController();
1316
+ const wsGroups = this.groupWebSocketRoutesByController();
1317
+ // Build log output with colors
1318
+ const lines = ['']; // Start with empty line for better spacing
1319
+ // 1. First, log HTTP-only controllers (sorted alphabetically)
1320
+ const httpOnlyControllers = Array.from(httpGroups.entries())
1321
+ .filter(([controllerName]) => !wsGroups.has(controllerName))
1322
+ .sort(([a], [b]) => a.localeCompare(b));
1323
+ for (const [controllerName, group] of httpOnlyControllers) {
1324
+ lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
1325
+ // Sort routes: GET first, then POST, PUT, PATCH, DELETE
1326
+ const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
1327
+ const sortedRoutes = group.routes.sort((a, b) => {
1328
+ const orderA = methodOrder.indexOf(a.method);
1329
+ const orderB = methodOrder.indexOf(b.method);
1330
+ return orderA - orderB;
1331
+ });
1332
+ for (const route of sortedRoutes) {
1333
+ // Colorize method based on type
1334
+ let coloredMethod = route.method;
1335
+ if (route.method === 'GET') {
1336
+ coloredMethod = green(route.method);
1337
+ }
1338
+ else if (route.method === 'POST') {
1339
+ coloredMethod = blue(route.method);
1340
+ }
1341
+ else if (route.method === 'PUT') {
1342
+ coloredMethod = yellow(route.method);
1343
+ }
1344
+ else if (route.method === 'DELETE') {
1345
+ coloredMethod = red(route.method);
1346
+ }
1347
+ lines.push(` ${coloredMethod} ${route.path}`);
1348
+ }
1349
+ lines.push(''); // Empty line between controllers
1350
+ }
1351
+ // 2. Then, log mixed controllers (HTTP + WebSocket)
1352
+ const mixedControllers = Array.from(httpGroups.entries())
1353
+ .filter(([controllerName]) => wsGroups.has(controllerName))
1354
+ .sort(([a], [b]) => a.localeCompare(b));
1355
+ for (const [controllerName, group] of mixedControllers) {
1356
+ // Merge WebSocket routes into this controller
1357
+ const wsGroup = wsGroups.get(controllerName);
1358
+ const allRoutes = [
1359
+ ...group.routes,
1360
+ ...wsGroup.routes.map((r) => ({ method: 'WS', path: r.path })),
1361
+ ];
1362
+ lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
1363
+ // Sort routes: GET first, then POST, PUT, PATCH, DELETE, WS
1364
+ const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'WS'];
1365
+ const sortedRoutes = allRoutes.sort((a, b) => {
1366
+ const orderA = methodOrder.indexOf(a.method);
1367
+ const orderB = methodOrder.indexOf(b.method);
1368
+ return orderA - orderB;
1369
+ });
1370
+ for (const route of sortedRoutes) {
1371
+ // Colorize method based on type
1372
+ let coloredMethod = route.method;
1373
+ if (route.method === 'GET') {
1374
+ coloredMethod = green(route.method);
1375
+ }
1376
+ else if (route.method === 'POST') {
1377
+ coloredMethod = blue(route.method);
1378
+ }
1379
+ else if (route.method === 'PUT') {
1380
+ coloredMethod = yellow(route.method);
1381
+ }
1382
+ else if (route.method === 'DELETE') {
1383
+ coloredMethod = red(route.method);
1384
+ }
1385
+ else if (route.method === 'WS') {
1386
+ coloredMethod = blue(route.method);
1387
+ }
1388
+ lines.push(` ${coloredMethod} ${route.path}`);
1389
+ }
1390
+ lines.push(''); // Empty line between controllers
1391
+ }
1392
+ // 3. Finally, log WebSocket-only namespaces
1393
+ const wsOnlyNamespaces = Array.from(wsGroups.entries())
1394
+ .filter(([controllerName]) => !httpGroups.has(controllerName))
1395
+ .sort(([a], [b]) => a.localeCompare(b));
1396
+ for (const [namespaceName, group] of wsOnlyNamespaces) {
1397
+ lines.push(` ${blue(namespaceName)} ${yellow(`(${group.basePath})`)}`);
1398
+ for (const route of group.routes) {
1399
+ lines.push(` ${blue('WS')} ${route.path}`);
1400
+ }
1401
+ lines.push(''); // Empty line between namespaces
1402
+ }
1403
+ return lines.join('\n');
1404
+ }
1405
+ }
1406
+ //# sourceMappingURL=Ergenecore.js.map