@asenajs/ergenecore 1.1.0 → 1.3.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 (49) hide show
  1. package/README.md +17 -17
  2. package/dist/lib/Ergenecore.d.ts +15 -2
  3. package/dist/lib/Ergenecore.js +152 -80
  4. package/dist/lib/Ergenecore.js.map +1 -1
  5. package/dist/lib/ErgenecoreContextWrapper.d.ts +44 -4
  6. package/dist/lib/ErgenecoreContextWrapper.js +115 -7
  7. package/dist/lib/ErgenecoreContextWrapper.js.map +1 -1
  8. package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +2 -16
  9. package/dist/lib/ErgenecoreWebsocketAdapter.js +24 -52
  10. package/dist/lib/ErgenecoreWebsocketAdapter.js.map +1 -1
  11. package/dist/lib/defaults/ConfigService.d.ts +5 -2
  12. package/dist/lib/defaults/ConfigService.js +1 -1
  13. package/dist/lib/defaults/ConfigService.js.map +1 -1
  14. package/dist/lib/defaults/MiddlewareService.d.ts +1 -1
  15. package/dist/lib/defaults/MiddlewareService.js +1 -1
  16. package/dist/lib/defaults/MiddlewareService.js.map +1 -1
  17. package/dist/lib/defaults/StaticServeService.d.ts +1 -1
  18. package/dist/lib/defaults/StaticServeService.js +1 -1
  19. package/dist/lib/defaults/ValidationService.d.ts +1 -1
  20. package/dist/lib/defaults/ValidationService.js +1 -1
  21. package/dist/lib/errors.d.ts +24 -5
  22. package/dist/lib/errors.js +15 -4
  23. package/dist/lib/errors.js.map +1 -1
  24. package/dist/lib/index.d.ts +5 -1
  25. package/dist/lib/index.js +6 -0
  26. package/dist/lib/index.js.map +1 -1
  27. package/dist/lib/middlewares/CorsMiddleware.d.ts +2 -2
  28. package/dist/lib/middlewares/CorsMiddleware.js +2 -2
  29. package/dist/lib/middlewares/CorsMiddleware.js.map +1 -1
  30. package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +2 -2
  31. package/dist/lib/middlewares/RateLimiterMiddleware.js +9 -6
  32. package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
  33. package/dist/lib/stream/SSEStreamWriter.d.ts +9 -0
  34. package/dist/lib/stream/SSEStreamWriter.js +18 -0
  35. package/dist/lib/stream/SSEStreamWriter.js.map +1 -0
  36. package/dist/lib/stream/StreamWriter.d.ts +24 -0
  37. package/dist/lib/stream/StreamWriter.js +72 -0
  38. package/dist/lib/stream/StreamWriter.js.map +1 -0
  39. package/dist/lib/stream/index.d.ts +2 -0
  40. package/dist/lib/stream/index.js +3 -0
  41. package/dist/lib/stream/index.js.map +1 -0
  42. package/dist/lib/types/SendPingStrategy.d.ts +4 -0
  43. package/dist/lib/types/SendPingStrategy.js +2 -0
  44. package/dist/lib/types/SendPingStrategy.js.map +1 -0
  45. package/dist/lib/types/Validation.d.ts +3 -3
  46. package/dist/lib/types/index.d.ts +1 -0
  47. package/dist/lib/types/index.js +2 -0
  48. package/dist/lib/types/index.js.map +1 -1
  49. package/package.json +19 -10
package/README.md CHANGED
@@ -70,8 +70,8 @@ await server.start();
70
70
  ### Controller Example
71
71
 
72
72
  ```typescript
73
- import { Controller } from '@asenajs/asena/server';
74
- import { Get, Post, Put, Delete } from '@asenajs/asena/web';
73
+ import { Controller } from '@asenajs/asena/decorators';
74
+ import { Get, Post, Put, Delete } from '@asenajs/asena/decorators/http';
75
75
  import type { Context } from '@asenajs/ergenecore/types';
76
76
 
77
77
  @Controller('/users')
@@ -225,9 +225,9 @@ const session = context.getCookie('session');
225
225
  #### Custom Middleware
226
226
 
227
227
  ```typescript
228
- import { Middleware } from '@asenajs/asena/server';
229
- import { Controller } from '@asenajs/asena/server';
230
- import { Get } from '@asenajs/asena/web';
228
+ import { Middleware } from '@asenajs/asena/decorators';
229
+ import { Controller } from '@asenajs/asena/decorators';
230
+ import { Get } from '@asenajs/asena/decorators/http';
231
231
  import { MiddlewareService, type Context } from '@asenajs/ergenecore';
232
232
 
233
233
  @Middleware()
@@ -262,7 +262,7 @@ export class AdminController {
262
262
  Ergenecore comes with a built-in CORS middleware for handling Cross-Origin Resource Sharing:
263
263
 
264
264
  ```typescript
265
- import { Middleware } from '@asenajs/asena/server';
265
+ import { Middleware } from '@asenajs/asena/decorators';
266
266
  import { CorsMiddleware } from '@asenajs/ergenecore';
267
267
 
268
268
  // Allow all origins (default)
@@ -303,7 +303,7 @@ export class DynamicCors extends CorsMiddleware {
303
303
  }
304
304
 
305
305
  // Use globally in server config
306
- import { Config } from '@asenajs/asena/server';
306
+ import { Config } from '@asenajs/asena/decorators';
307
307
  import { ConfigService } from '@asenajs/ergenecore';
308
308
 
309
309
  @Config()
@@ -326,7 +326,7 @@ export class ApiController {
326
326
  Ergenecore includes a Token Bucket-based rate limiter for controlling request rates and preventing abuse:
327
327
 
328
328
  ```typescript
329
- import { Middleware } from '@asenajs/asena/server';
329
+ import { Middleware } from '@asenajs/asena/decorators';
330
330
  import { RateLimiterMiddleware } from '@asenajs/ergenecore';
331
331
 
332
332
  // General API rate limiter (100 requests per minute)
@@ -381,7 +381,7 @@ export class CustomRateLimiter extends RateLimiterMiddleware {
381
381
  }
382
382
 
383
383
  // Use globally
384
- import { Config } from '@asenajs/asena/server';
384
+ import { Config } from '@asenajs/asena/decorators';
385
385
  import { ConfigService } from '@asenajs/ergenecore';
386
386
 
387
387
  @Config()
@@ -432,8 +432,8 @@ The middleware automatically sets standard rate limit headers:
432
432
  ### Validation with Zod
433
433
 
434
434
  ```typescript
435
- import { Controller, Middleware } from '@asenajs/asena/server';
436
- import { Post } from '@asenajs/asena/web';
435
+ import { Controller, Middleware } from '@asenajs/asena/decorators';
436
+ import { Post } from '@asenajs/asena/decorators/http';
437
437
  import { ValidationService, type Context, type ValidationSchema, type ValidationSchemaWithHook } from '@asenajs/ergenecore';
438
438
  import { z } from 'zod';
439
439
 
@@ -471,8 +471,8 @@ async json(): Promise<ValidationSchema | ValidationSchemaWithHook> {
471
471
  ### Combining Middleware and Validation
472
472
 
473
473
  ```typescript
474
- import { Controller, Middleware } from '@asenajs/asena/server';
475
- import { Post } from '@asenajs/asena/web';
474
+ import { Controller, Middleware } from '@asenajs/asena/decorators';
475
+ import { Post } from '@asenajs/asena/decorators/http';
476
476
  import {
477
477
  MiddlewareService,
478
478
  ValidationService,
@@ -556,8 +556,8 @@ export class ChatSocket extends AsenaWebSocketService<void> {
556
556
  ### Static File Serving
557
557
 
558
558
  ```typescript
559
- import { Controller } from '@asenajs/asena/server';
560
- import { Get } from '@asenajs/asena/web';
559
+ import { Controller } from '@asenajs/asena/decorators';
560
+ import { Get } from '@asenajs/asena/decorators/http';
561
561
  import { StaticServe, StaticServeService } from '@asenajs/asena/static';
562
562
  import type { Context } from '@asenajs/ergenecore/types';
563
563
 
@@ -587,8 +587,8 @@ export class StaticController {
587
587
  ### Error Handling
588
588
 
589
589
  ```typescript
590
- import { Config } from '@asenajs/asena/server';
591
- import { Inject } from '@asenajs/asena/ioc';
590
+ import { Config } from '@asenajs/asena/decorators';
591
+ import { Inject } from '@asenajs/asena/decorators/ioc';
592
592
  import { ConfigService, type Context } from '@asenajs/ergenecore';
593
593
 
594
594
  @Config()
@@ -56,6 +56,11 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
56
56
  * Config is optional - if not provided, middleware applies to all routes
57
57
  */
58
58
  private globalMiddlewares;
59
+ /**
60
+ * HTML routes for FrontendController pages
61
+ * Stored separately and merged into Bun.serve() routes at start time
62
+ */
63
+ private htmlRoutes;
59
64
  private options;
60
65
  /**
61
66
  * Creates a new CoreAdapter instance
@@ -82,7 +87,7 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
82
87
  * });
83
88
  * ```
84
89
  */
85
- registerWebsocketRoute(params: WebsocketRouteParams<Context>): void;
90
+ registerWebsocketRoute(params: WebsocketRouteParams<Context>): Promise<void>;
86
91
  /**
87
92
  * Sets the server port
88
93
  *
@@ -107,6 +112,14 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
107
112
  * });
108
113
  * ```
109
114
  */
115
+ /**
116
+ * Registers an HTML route for FrontendController pages.
117
+ * HTML routes bypass the middleware chain and are served directly by Bun.serve().
118
+ *
119
+ * @param path - Full URL path (e.g., '/ui/home')
120
+ * @param htmlBundle - The HTML bundle returned by importing an .html file
121
+ */
122
+ registerHTMLRoute(path: string, htmlBundle: unknown): void;
110
123
  use(middleware: BaseMiddleware<Context>, config?: GlobalMiddlewareConfig['routes']): void;
111
124
  /**
112
125
  * Registers a route (deferred registration)
@@ -143,7 +156,7 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
143
156
  * @param port - Optional port to override default
144
157
  * @returns Bun server instance
145
158
  */
146
- start(port?: number): Server<any>;
159
+ start(port?: number): Promise<Server<any>>;
147
160
  /**
148
161
  * Stops the server
149
162
  *
@@ -1,11 +1,11 @@
1
1
  import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
2
2
  import { blue, green, red, yellow } from '@asenajs/asena/logger';
3
- import { shouldApplyMiddleware } from '@asenajs/asena/utils/patternMatcher';
4
3
  import { ErgenecoreWebsocketAdapter } from './ErgenecoreWebsocketAdapter';
5
4
  import { ErgenecoreContextWrapper } from './ErgenecoreContextWrapper';
6
5
  import * as Bun from 'bun';
7
6
  import * as path from 'path';
8
7
  import { HttpException, MiddlewareResponseError } from './errors';
8
+ import { shouldApplyMiddleware } from '@asenajs/asena/utils';
9
9
  /**
10
10
  * Static response headers for performance
11
11
  *
@@ -63,6 +63,11 @@ export class Ergenecore extends AsenaAdapter {
63
63
  * Config is optional - if not provided, middleware applies to all routes
64
64
  */
65
65
  globalMiddlewares = [];
66
+ /**
67
+ * HTML routes for FrontendController pages
68
+ * Stored separately and merged into Bun.serve() routes at start time
69
+ */
70
+ htmlRoutes = new Map();
66
71
  options = {};
67
72
  /**
68
73
  * Creates a new CoreAdapter instance
@@ -92,12 +97,12 @@ export class Ergenecore extends AsenaAdapter {
92
97
  * });
93
98
  * ```
94
99
  */
95
- registerWebsocketRoute(params) {
100
+ async registerWebsocketRoute(params) {
96
101
  // Queue WebSocket route for building during start()
97
102
  this.wsRouteQueue.push(params);
98
103
  // Register WebSocket service with adapter using the route path (not namespace)
99
104
  if (this.websocketAdapter && params.websocketService) {
100
- this.websocketAdapter.registerWebSocket(params.websocketService);
105
+ await this.websocketAdapter.registerWebSocket(params.websocketService);
101
106
  }
102
107
  }
103
108
  /**
@@ -126,6 +131,27 @@ export class Ergenecore extends AsenaAdapter {
126
131
  * });
127
132
  * ```
128
133
  */
134
+ /**
135
+ * Registers an HTML route for FrontendController pages.
136
+ * HTML routes bypass the middleware chain and are served directly by Bun.serve().
137
+ *
138
+ * @param path - Full URL path (e.g., '/ui/home')
139
+ * @param htmlBundle - The HTML bundle returned by importing an .html file
140
+ */
141
+ registerHTMLRoute(path, htmlBundle) {
142
+ if (this.htmlRoutes.has(path)) {
143
+ throw new Error(`Duplicate HTML route: "${path}" is already registered.`);
144
+ }
145
+ this.htmlRoutes.set(path, htmlBundle);
146
+ // Register trailing slash variant for consistent routing
147
+ // e.g., /ui → also register /ui/ (or vice versa)
148
+ if (path !== '/' && !path.endsWith('/')) {
149
+ this.htmlRoutes.set(`${path}/`, htmlBundle);
150
+ }
151
+ else if (path !== '/' && path.endsWith('/')) {
152
+ this.htmlRoutes.set(path.slice(0, -1), htmlBundle);
153
+ }
154
+ }
129
155
  use(middleware, config) {
130
156
  this.globalMiddlewares.push({ middleware, config });
131
157
  }
@@ -166,7 +192,7 @@ export class Ergenecore extends AsenaAdapter {
166
192
  * @param port - Optional port to override default
167
193
  * @returns Bun server instance
168
194
  */
169
- start(port) {
195
+ async start(port) {
170
196
  // Build routes if not built yet
171
197
  const serverHostname = this._hostname;
172
198
  if (!this.routesBuilt) {
@@ -178,8 +204,15 @@ export class Ergenecore extends AsenaAdapter {
178
204
  this.checkPathCollisions(httpRoutes, wsRoutes);
179
205
  // 4. Merge routes
180
206
  const finalRoutes = this.mergeRoutes(httpRoutes, wsRoutes);
207
+ // 4.5. Merge HTML routes (FrontendController pages)
208
+ for (const [htmlPath, htmlBundle] of this.htmlRoutes) {
209
+ if (finalRoutes[htmlPath]) {
210
+ throw new Error(`HTML route collision at "${htmlPath}": path already registered as an API or WebSocket route.`);
211
+ }
212
+ finalRoutes[htmlPath] = htmlBundle;
213
+ }
181
214
  // 5. Prepare WebSocket before starting server
182
- this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
215
+ await this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
183
216
  const serverPort = port ?? this.port;
184
217
  // 6. Start Bun server with merged routes
185
218
  this.server = Bun.serve({
@@ -190,7 +223,7 @@ export class Ergenecore extends AsenaAdapter {
190
223
  websocket: this.websocketAdapter.websocket,
191
224
  });
192
225
  // Start WebSocket server (initializes AsenaWebSocketServer for each namespace)
193
- this.websocketAdapter.startWebsocket(this.server);
226
+ await this.websocketAdapter.startWebsocket(this.server);
194
227
  this.routesBuilt = true;
195
228
  // Log controller summary first
196
229
  if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
@@ -275,7 +308,15 @@ export class Ergenecore extends AsenaAdapter {
275
308
  }
276
309
  // Build Bun router object for each path
277
310
  for (const [path, pathRoutes] of routesByPath) {
278
- routes[path] = {};
311
+ // Normalize trailing slash to support both variants
312
+ // This ensures /users and /users/ both work with query params
313
+ const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
314
+ const pathWithSlash = normalizedPath === '/' ? '/' : `${normalizedPath}/`;
315
+ // Initialize both path variants (skip duplicate for root path)
316
+ routes[normalizedPath] = routes[normalizedPath] || {};
317
+ if (normalizedPath !== '/') {
318
+ routes[pathWithSlash] = routes[pathWithSlash] || {};
319
+ }
279
320
  for (const route of pathRoutes) {
280
321
  const method = route.method.toUpperCase();
281
322
  // Fast Path Optimization with Pattern Matching
@@ -286,19 +327,48 @@ export class Ergenecore extends AsenaAdapter {
286
327
  (!route.middlewares || route.middlewares.length === 0) &&
287
328
  !route.validator &&
288
329
  !route.staticServe;
289
- if (isSimpleRoute) {
290
- // Use fast path handler for simple routes (minimal overhead)
291
- routes[path][method] = this.createFastPathHandler(route);
330
+ // Create handler based on route complexity
331
+ const handler = isSimpleRoute
332
+ ? this.createFastPathHandler(route)
333
+ : this.createRouteHandler(route, commonMiddlewares);
334
+ // Register handler to both path variants for trailing slash support
335
+ if (method === 'ALL') {
336
+ // Bun native: direct handler function matches all HTTP methods
337
+ routes[normalizedPath] = handler;
338
+ if (normalizedPath !== '/') {
339
+ routes[pathWithSlash] = handler;
340
+ }
292
341
  }
293
342
  else {
294
- // Use full-featured handler for complex routes
295
- routes[path][method] = this.createRouteHandler(route, commonMiddlewares);
343
+ routes[normalizedPath][method] = handler;
344
+ if (normalizedPath !== '/') {
345
+ routes[pathWithSlash][method] = handler;
346
+ }
296
347
  }
297
348
  }
298
349
  }
299
350
  }
300
- // Add 404 handler with static headers
301
- routes['/*'] = () => {
351
+ // Catch-all handler for unmatched routes (including OPTIONS preflight)
352
+ // Runs global middlewares so CORS and other cross-cutting concerns work for all requests
353
+ routes['/*'] = async (req) => {
354
+ const context = new ErgenecoreContextWrapper(req, this.server);
355
+ const requestPath = new URL(req.url).pathname;
356
+ // Filter and run applicable global middlewares
357
+ const applicableMiddlewares = this.globalMiddlewares
358
+ .filter(({ config }) => shouldApplyMiddleware(requestPath, config))
359
+ .map(({ middleware }) => middleware);
360
+ if (applicableMiddlewares.length > 0) {
361
+ const result = await this.executeMiddlewares(context, applicableMiddlewares);
362
+ if (result instanceof Response)
363
+ return result;
364
+ if (result === false) {
365
+ return new Response('Forbidden', { status: 403 });
366
+ }
367
+ }
368
+ // No middleware handled the request → error handler or 404
369
+ if (this.errorHandler) {
370
+ return this.errorHandler(new Error('Not Found'), context);
371
+ }
302
372
  return new Response(JSON.stringify({ error: 'Not Found' }), {
303
373
  status: 404,
304
374
  headers: STATIC_JSON_HEADERS,
@@ -333,7 +403,7 @@ export class Ergenecore extends AsenaAdapter {
333
403
  // Initialize path object
334
404
  routes[path] = routes[path] || {};
335
405
  // Add GET handler for WebSocket upgrade (with middleware chain)
336
- routes[path]['GET'] = this.createWebSocketUpgradeHandler(wsRoute);
406
+ routes[path].GET = this.createWebSocketUpgradeHandler(wsRoute);
337
407
  }
338
408
  return routes;
339
409
  }
@@ -363,15 +433,24 @@ export class Ergenecore extends AsenaAdapter {
363
433
  * }
364
434
  * ```
365
435
  */
366
- async executeMiddlewares(context, middlewares, index = 0) {
436
+ async executeMiddlewares(context, middlewares, index = 0, onComplete) {
367
437
  // Base case: all middlewares executed successfully
368
438
  if (index >= middlewares.length) {
439
+ if (onComplete) {
440
+ const result = await onComplete();
441
+ if (result instanceof Response)
442
+ return result;
443
+ if (result === false)
444
+ return false;
445
+ }
369
446
  return true;
370
447
  }
371
448
  const middleware = middlewares[index];
449
+ let nextCalled = false;
372
450
  // Create next() function that executes the next middleware in chain
373
451
  const next = async () => {
374
- const result = await this.executeMiddlewares(context, middlewares, index + 1);
452
+ nextCalled = true;
453
+ const result = await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
375
454
  // If next middleware returned Response, throw MiddlewareResponseError to propagate it
376
455
  if (result instanceof Response) {
377
456
  throw new MiddlewareResponseError(result);
@@ -392,7 +471,11 @@ export class Ergenecore extends AsenaAdapter {
392
471
  if (result === false) {
393
472
  return false;
394
473
  }
395
- // Middleware executed successfully
474
+ // If middleware returned true/void without calling next(), auto-continue the chain
475
+ if (!nextCalled) {
476
+ return await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
477
+ }
478
+ // Middleware called next() and completed successfully
396
479
  return true;
397
480
  }
398
481
  catch (error) {
@@ -442,7 +525,7 @@ export class Ergenecore extends AsenaAdapter {
442
525
  return async (req) => {
443
526
  try {
444
527
  // Create context wrapper
445
- const context = new ErgenecoreContextWrapper(req);
528
+ const context = new ErgenecoreContextWrapper(req, this.server);
446
529
  // Execute filtered global middlewares with real next() chain
447
530
  if (applicableGlobalMiddlewares.length > 0) {
448
531
  const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
@@ -522,7 +605,7 @@ export class Ergenecore extends AsenaAdapter {
522
605
  // Check if HTTP routes have the same path
523
606
  if (httpRoutes[wsPath]) {
524
607
  // Check if HTTP route has GET method (collision with WebSocket GET)
525
- if (httpRoutes[wsPath]['GET']) {
608
+ if (httpRoutes[wsPath].GET) {
526
609
  throw new Error(`Route collision detected at path "${wsPath}": Both HTTP and WebSocket routes define GET method. ` +
527
610
  `WebSocket routes use GET for upgrade handshake, so HTTP GET cannot be registered on the same path.`);
528
611
  }
@@ -583,14 +666,13 @@ export class Ergenecore extends AsenaAdapter {
583
666
  // If no error handler is set, use ultra-minimal version with default error handling
584
667
  if (!this.errorHandler) {
585
668
  return async (req) => {
586
- const context = new ErgenecoreContextWrapper(req);
669
+ const context = new ErgenecoreContextWrapper(req, this.server);
587
670
  try {
588
671
  // Inject Bun's native route params if present
589
- // @ts-ignore - Bun adds params to Request
672
+ // @ts-expect-error - Bun adds params to Request
590
673
  if (req.params) {
591
- // @ts-ignore - Bun adds params to Request
674
+ // @ts-expect-error - Bun adds params to Request
592
675
  const params = req.params;
593
- // eslint-disable-next-line guard-for-in
594
676
  for (const key in params) {
595
677
  context.setValue(`param:${key}`, params[key]);
596
678
  }
@@ -625,14 +707,13 @@ export class Ergenecore extends AsenaAdapter {
625
707
  }
626
708
  // With error handler (minimal try-catch)
627
709
  return async (req) => {
628
- const context = new ErgenecoreContextWrapper(req);
710
+ const context = new ErgenecoreContextWrapper(req, this.server);
629
711
  try {
630
712
  // Inject Bun's native route params if present
631
- // @ts-ignore - Bun adds params to Request
713
+ // @ts-expect-error - Bun adds params to Request
632
714
  if (req.params) {
633
- // @ts-ignore - Bun adds params to Request
715
+ // @ts-expect-error - Bun adds params to Request
634
716
  const params = req.params;
635
- // eslint-disable-next-line guard-for-in
636
717
  for (const key in params) {
637
718
  context.setValue(`param:${key}`, params[key]);
638
719
  }
@@ -678,65 +759,59 @@ export class Ergenecore extends AsenaAdapter {
678
759
  const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
679
760
  return async (req) => {
680
761
  // Create context wrapper outside try block so it's accessible in catch
681
- const context = new ErgenecoreContextWrapper(req);
762
+ const context = new ErgenecoreContextWrapper(req, this.server);
682
763
  // Inject Bun's native route params
683
- // @ts-ignore - Bun adds params to Request
764
+ // @ts-expect-error - Bun adds params to Request
684
765
  if (req.params) {
685
- // @ts-ignore - Bun adds params to Request
766
+ // @ts-expect-error - Bun adds params to Request
686
767
  const params = req.params;
687
- // eslint-disable-next-line guard-for-in
688
768
  for (const key in params) {
689
769
  context.setValue(`param:${key}`, params[key]);
690
770
  }
691
771
  }
692
772
  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 });
773
+ // Combine global + route middlewares into a single chain
774
+ const allMiddlewares = [
775
+ ...applicableGlobalMiddlewares,
776
+ ...(route.middlewares || []),
777
+ ];
778
+ // Execute middleware chain with handler as onComplete callback
779
+ // This ensures the handler runs INSIDE the middleware async context
780
+ const result = await this.executeMiddlewares(context, allMiddlewares, 0, async () => {
781
+ // Execute validation
782
+ if (route.validator) {
783
+ const validationResult = await this.validateRequest(context, route.validator);
784
+ if (validationResult)
785
+ return validationResult;
703
786
  }
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;
787
+ // Handle static file serving
788
+ if (route.staticServe) {
789
+ const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
790
+ if (staticResponse)
791
+ return staticResponse;
711
792
  }
712
- // If middleware returned false, return 403
713
- if (result === false) {
714
- return new Response('Forbidden', { status: 403 });
793
+ // Execute route handler
794
+ const response = await route.handler(context);
795
+ // If handler returns Response, return it directly
796
+ if (response instanceof Response) {
797
+ return response;
715
798
  }
799
+ // Otherwise, wrap in Response
800
+ return new Response(JSON.stringify(response), {
801
+ status: 200,
802
+ headers: { 'Content-Type': 'application/json' },
803
+ });
804
+ });
805
+ // If middleware chain returned a custom response, return it
806
+ if (result instanceof Response) {
807
+ return result;
716
808
  }
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;
809
+ // If middleware chain returned false (stopped), return 403
810
+ if (result === false) {
811
+ return new Response('Forbidden', { status: 403 });
728
812
  }
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
- });
813
+ // Default: result is true (should not happen with onComplete, but just in case)
814
+ return new Response(null, { status: 204 });
740
815
  }
741
816
  catch (error) {
742
817
  // If handler or middleware threw HttpException, convert to Response
@@ -746,7 +821,7 @@ export class Ergenecore extends AsenaAdapter {
746
821
  // Handle other errors with custom error handler if available
747
822
  if (this.errorHandler) {
748
823
  // Pass the original context with params already injected
749
- return await this.errorHandler(error, context);
824
+ return this.errorHandler(error, context);
750
825
  }
751
826
  // Default error response
752
827
  this.logger.error('Route handler error:', error);
@@ -1057,7 +1132,7 @@ export class Ergenecore extends AsenaAdapter {
1057
1132
  */
1058
1133
  extractBasePath(path) {
1059
1134
  // Remove trailing slash (except for root)
1060
- let normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
1135
+ const normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
1061
1136
  // Split path into segments
1062
1137
  const segments = normalized.split('/');
1063
1138
  const baseSegments = [];
@@ -1355,10 +1430,7 @@ export class Ergenecore extends AsenaAdapter {
1355
1430
  for (const [controllerName, group] of mixedControllers) {
1356
1431
  // Merge WebSocket routes into this controller
1357
1432
  const wsGroup = wsGroups.get(controllerName);
1358
- const allRoutes = [
1359
- ...group.routes,
1360
- ...wsGroup.routes.map((r) => ({ method: 'WS', path: r.path })),
1361
- ];
1433
+ const allRoutes = [...group.routes, ...wsGroup.routes.map((r) => ({ method: 'WS', path: r.path }))];
1362
1434
  lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
1363
1435
  // Sort routes: GET first, then POST, PUT, PATCH, DELETE, WS
1364
1436
  const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'WS'];