@asenajs/ergenecore 1.4.0 → 2.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.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Asena Ergenecore Adapter
2
2
 
3
+ [![Version](https://img.shields.io/badge/version-2.0.0-blue.svg)](https://asena.sh)
4
+ [![Bun Version](https://img.shields.io/badge/Bun-1.3.12%2B-blueviolet)](https://bun.sh)
5
+
3
6
  **Blazing-fast** native Bun adapter for [Asena.js](https://github.com/asenajs/asena) - Built exclusively with Bun's native APIs for maximum performance.
4
7
 
5
8
  Built on Bun runtime for exceptional performance:
@@ -604,11 +607,14 @@ export class ServerConfig extends ConfigService {
604
607
  }
605
608
 
606
609
  // With custom exception mapper
610
+ import { isValidationError } from '@asenajs/asena/adapter';
611
+
607
612
  export class ExceptionMapper {
608
613
  map(error: Error, context: Context): Response {
609
- // Custom error mapping logic
610
- if (error.name === 'ValidationError') {
611
- return context.send({ error: 'Validation failed', details: error.message }, 400);
614
+ // Request validation failures arrive here like any other error.
615
+ // Check this before `instanceof HttpException` - ValidationError extends it
616
+ if (isValidationError(error)) {
617
+ return context.send({ error: 'Validation failed', details: error.issues }, 400);
612
618
  }
613
619
 
614
620
  return context.send({ error: 'Internal server error' }, 500);
@@ -1,5 +1,5 @@
1
1
  import type { WebsocketRouteParams } from '@asenajs/asena/adapter';
2
- import { AsenaAdapter, type AsenaServeOptions, type AsenaStartOptions, type BaseMiddleware, type ErrorHandler, type RouteParams } from '@asenajs/asena/adapter';
2
+ import { AsenaAdapter, type AsenaServeOptions, type AsenaStartOptions, type BaseMiddleware, type ErrorHandler, type NotFoundHandler, type RouteParams } from '@asenajs/asena/adapter';
3
3
  import { type ServerLogger } from '@asenajs/asena/logger';
4
4
  import type { GlobalMiddlewareConfig } from '@asenajs/asena/server/config';
5
5
  import { ErgenecoreWebsocketAdapter } from './ErgenecoreWebsocketAdapter';
@@ -50,6 +50,15 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
50
50
  * Error handler function
51
51
  */
52
52
  private errorHandler?;
53
+ /**
54
+ * Handler for requests that matched no route
55
+ */
56
+ private notFoundHandler?;
57
+ /**
58
+ * Whether the adapter logs errors before handing them to the application.
59
+ * Set false when your own handler already logs with a correlation id.
60
+ */
61
+ private readonly logErrors;
53
62
  /**
54
63
  * Global middlewares with route configuration
55
64
  * Structure: Array<{ middleware, config }>
@@ -72,7 +81,7 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
72
81
  * @param logger - Server logger instance
73
82
  * @param websocketAdapter - WebSocket adapter instance (optional)
74
83
  */
75
- constructor(logger: ServerLogger, websocketAdapter?: ErgenecoreWebsocketAdapter);
84
+ constructor(logger: ServerLogger, websocketAdapter?: ErgenecoreWebsocketAdapter, logErrors?: boolean);
76
85
  /**
77
86
  * Registers a WebSocket route (deferred registration)
78
87
  *
@@ -173,6 +182,12 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
173
182
  * @param errorHandler - Error handler function
174
183
  */
175
184
  onError(errorHandler: ErrorHandler<Context>): void;
185
+ /**
186
+ * Registers the handler for requests that match no route.
187
+ *
188
+ * Separate from {@link onError}: a missing route is a routing outcome, not a thrown error.
189
+ */
190
+ onNotFound(notFoundHandler: NotFoundHandler<Context>): void;
176
191
  /**
177
192
  * Sets serve options
178
193
  *
@@ -223,6 +238,54 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
223
238
  * ```
224
239
  */
225
240
  private buildWebSocketRoutes;
241
+ /**
242
+ * Answers a request nothing served: an unmatched route, or a static file that does not exist.
243
+ *
244
+ * Both go through the application's `onNotFound` so an app has one place to shape its 404,
245
+ * and both fall back to the same JSON envelope the hono adapter uses.
246
+ *
247
+ * Deliberately NOT routed to `onError` - that hook is for errors the application threw.
248
+ */
249
+ private respondToUnmatched;
250
+ private resolveUnmatchedResponse;
251
+ /**
252
+ * Records a request the framework itself answered with a 404.
253
+ *
254
+ * Only reached when the application has no `onNotFound`, or when its hook declined or threw -
255
+ * an application that answered its own 404 already knows about the request. Without this an
256
+ * unmatched route was the one outcome that produced no output at any level, which is exactly
257
+ * the traffic (bots, probes, a typo in a deployed client) an operator needs to see.
258
+ *
259
+ * `info`, not `warn`: a scanner walking /wp-admin, /.env and /phpmyadmin must not be able to
260
+ * fill the warning stream. Not `debug` either - a 404 nobody can see is how a mistyped route
261
+ * survives to production.
262
+ */
263
+ private logUnmatched;
264
+ /**
265
+ * Logs an error before the application's handler sees it, at a level that matches the
266
+ * response the caller will get.
267
+ *
268
+ * This adapter used to log only when NO error handler was registered - which is to say,
269
+ * never in a real application, since every app configures `onError`. A 500 answered
270
+ * nothing to stdout and the stack was gone. The level split mirrors the hono adapter:
271
+ * 5xx is ours to fix and gets a stack, 4xx is the caller's and would otherwise let a bot
272
+ * scanning for /wp-admin flood the error stream.
273
+ */
274
+ private logHandledError;
275
+ /**
276
+ * Produces the response for a thrown error, in the same order the hono adapter uses.
277
+ *
278
+ * The application's handler gets first refusal on *every* error, `HttpException` included.
279
+ * Previously each catch block answered an `HttpException` straight from `getResponse()` and
280
+ * only consulted `errorHandler` for everything else, so an app could not reshape its own
281
+ * 401/403/404 envelopes. Worse, only one of the five catch blocks carried the
282
+ * `!isValidationError` exemption, so a `ValidationError` thrown on the fast path never
283
+ * reached `onError` at all - the exact opposite of why ValidationError exists.
284
+ *
285
+ * Falls back to the exception's own response (or a 500) when there is no handler, when the
286
+ * handler returns nothing, or when the handler itself throws.
287
+ */
288
+ private respondToError;
226
289
  /**
227
290
  * Executes middleware chain with recursive next() pattern
228
291
  *
@@ -342,7 +405,6 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
342
405
  * - Error handling
343
406
  *
344
407
  * @param route - Route parameters
345
- * @param _commonMiddlewares - Common middlewares for this route group (for future optimization)
346
408
  * @returns Bun-compatible route handler
347
409
  */
348
410
  private createRouteHandler;
@@ -438,66 +500,6 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
438
500
  * ```
439
501
  */
440
502
  private buildStaticFileHeaders;
441
- /**
442
- * Extracts base path from a route path
443
- *
444
- * Removes dynamic segments (parameters and wildcards) from the path
445
- * to find the static base path for grouping routes.
446
- *
447
- * @param path - Route path (e.g., "/api/users/:id")
448
- * @returns Base path without dynamic segments (e.g., "/api/users")
449
- *
450
- * @example
451
- * ```typescript
452
- * extractBasePath('/api/users/:id') // => '/api/users'
453
- * extractBasePath('/api/users/:id/posts/:postId') // => '/api/users'
454
- * extractBasePath('/static/*') // => '/static'
455
- * extractBasePath('/') // => '/'
456
- * ```
457
- */
458
- private extractBasePath;
459
- /**
460
- * Extracts common middlewares across multiple routes
461
- *
462
- * Identifies middlewares that are present in ALL routes and can be
463
- * moved to group level for optimization.
464
- *
465
- * @param routes - Array of routes to analyze
466
- * @returns Array of common middleware instances
467
- *
468
- * @example
469
- * ```typescript
470
- * extractCommonMiddlewares([
471
- * { middlewares: [auth, log, rate] },
472
- * { middlewares: [auth, log] },
473
- * { middlewares: [auth, log] }
474
- * ]) // => [auth, log]
475
- * ```
476
- */
477
- private extractCommonMiddlewares;
478
- /**
479
- * Groups routes by their base path
480
- *
481
- * Creates a map of base paths to routes for optimization.
482
- * Routes with the same base path will share common middlewares.
483
- *
484
- * @param routes - Array of routes to group
485
- * @returns Map of base paths to route arrays
486
- *
487
- * @example
488
- * ```typescript
489
- * groupRoutesByBasePath([
490
- * { path: '/api/users' },
491
- * { path: '/api/users/:id' },
492
- * { path: '/api/posts' }
493
- * ])
494
- * // => Map {
495
- * // '/api/users' => [route1, route2],
496
- * // '/api/posts' => [route3]
497
- * // }
498
- * ```
499
- */
500
- private groupRoutesByBasePath;
501
503
  get hostname(): any;
502
504
  set hostname(value: any);
503
505
  /**