@asenajs/ergenecore 1.5.0 → 3.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-3.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:
@@ -17,7 +20,7 @@ Built on Bun runtime for exceptional performance:
17
20
 
18
21
  ## Features
19
22
 
20
- - ⚡ **Zero Dependencies** - Pure Bun native APIs (except Zod for validation)
23
+ - ⚡ **Zero Dependencies** - no runtime dependencies at all; Zod is a peer your project owns
21
24
  - 🚀 **SIMD-Accelerated** - Native Bun router with SIMD-optimized matching
22
25
  - 🔌 **Full Adapter Support** - HTTP, WebSocket, Middleware, Validation
23
26
  - 📦 **Zero-Copy File Serving** - Bun.file() for optimal static file performance
@@ -31,20 +34,31 @@ Ergenecore is the **fastest** Asena adapter because it:
31
34
  - Uses Bun's **native HTTP server** (`Bun.serve()`)
32
35
  - Leverages **SIMD-accelerated routing** (no framework overhead)
33
36
  - Implements **zero-copy** file serving with `Bun.file()`
34
- - Has **no external dependencies** (except Zod)
37
+ - Has **no runtime dependencies at all** (Zod is a peer your project owns)
35
38
  - Is optimized for **Bun runtime** exclusively
36
39
 
37
40
  ## Requirements
38
41
 
39
42
  - [Bun](https://bun.sh) v1.3.12 or higher
43
+ - [@asenajs/asena](https://github.com/AsenaJs/Asena) v0.10.0 or higher (peer dependency)
44
+ - [Zod](https://zod.dev) v4.3.6 or higher (peer dependency)
40
45
  - TypeScript v5.8.2 or higher
41
46
 
42
47
  ## Installation
43
48
 
44
49
  ```bash
45
- bun add @asenajs/ergenecore
50
+ bun add @asenajs/ergenecore zod
46
51
  ```
47
52
 
53
+ `zod` is a **peer dependency** - the adapter defines the validation contract, your project owns the
54
+ library and its version.
55
+
56
+ | Package manager | Command |
57
+ |:--|:--|
58
+ | bun, npm 7+, pnpm 8+ | `bun add zod` — peers auto-install, but declare them anyway so they survive a clean install |
59
+ | yarn 1 | `yarn add zod` — **required**, yarn 1 does not install peers for you |
60
+
61
+
48
62
  ## Quick Start
49
63
 
50
64
  ### Basic Server Setup
@@ -609,7 +623,8 @@ import { isValidationError } from '@asenajs/asena/adapter';
609
623
  export class ExceptionMapper {
610
624
  map(error: Error, context: Context): Response {
611
625
  // Request validation failures arrive here like any other error.
612
- // Check this before `instanceof HttpException` - ValidationError extends it
626
+ // Check this before the generic `isHttpException()` branch - a ValidationError is an
627
+ // HTTP exception too, so the generic branch would otherwise swallow it
613
628
  if (isValidationError(error)) {
614
629
  return context.send({ error: 'Validation failed', details: error.issues }, 400);
615
630
  }
@@ -635,7 +650,7 @@ Contributions are welcome! Please follow these guidelines:
635
650
 
636
651
  1. Follow the **TDD approach** (write tests first)
637
652
  2. Maintain **90%+ test coverage**
638
- 3. Use **Bun native APIs** only (no external dependencies except Zod)
653
+ 3. Use **Bun native APIs** only (no runtime dependencies; Zod is a peer)
639
654
  4. Follow the **CLAUDE.md** guidelines
640
655
 
641
656
  ## License
@@ -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
  *
@@ -164,6 +173,11 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
164
173
  /**
165
174
  * Stops the server
166
175
  *
176
+ * The HTTP socket closes first and the WebSocket layer second - the reverse of the order
177
+ * start() brought them up, since the transport is initialised *after* Bun.serve(). A close
178
+ * handler running during the drain may still broadcast, so the transport has to outlive the
179
+ * sockets that use it.
180
+ *
167
181
  * @param closeActiveConnections - Whether to close active connections
168
182
  */
169
183
  stop(closeActiveConnections?: boolean): Promise<void>;
@@ -173,6 +187,12 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
173
187
  * @param errorHandler - Error handler function
174
188
  */
175
189
  onError(errorHandler: ErrorHandler<Context>): void;
190
+ /**
191
+ * Registers the handler for requests that match no route.
192
+ *
193
+ * Separate from {@link onError}: a missing route is a routing outcome, not a thrown error.
194
+ */
195
+ onNotFound(notFoundHandler: NotFoundHandler<Context>): void;
176
196
  /**
177
197
  * Sets serve options
178
198
  *
@@ -223,6 +243,54 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
223
243
  * ```
224
244
  */
225
245
  private buildWebSocketRoutes;
246
+ /**
247
+ * Answers a request nothing served: an unmatched route, or a static file that does not exist.
248
+ *
249
+ * Both go through the application's `onNotFound` so an app has one place to shape its 404,
250
+ * and both fall back to the same JSON envelope the hono adapter uses.
251
+ *
252
+ * Deliberately NOT routed to `onError` - that hook is for errors the application threw.
253
+ */
254
+ private respondToUnmatched;
255
+ private resolveUnmatchedResponse;
256
+ /**
257
+ * Records a request the framework itself answered with a 404.
258
+ *
259
+ * Only reached when the application has no `onNotFound`, or when its hook declined or threw -
260
+ * an application that answered its own 404 already knows about the request. Without this an
261
+ * unmatched route was the one outcome that produced no output at any level, which is exactly
262
+ * the traffic (bots, probes, a typo in a deployed client) an operator needs to see.
263
+ *
264
+ * `info`, not `warn`: a scanner walking /wp-admin, /.env and /phpmyadmin must not be able to
265
+ * fill the warning stream. Not `debug` either - a 404 nobody can see is how a mistyped route
266
+ * survives to production.
267
+ */
268
+ private logUnmatched;
269
+ /**
270
+ * Logs an error before the application's handler sees it, at a level that matches the
271
+ * response the caller will get.
272
+ *
273
+ * This adapter used to log only when NO error handler was registered - which is to say,
274
+ * never in a real application, since every app configures `onError`. A 500 answered
275
+ * nothing to stdout and the stack was gone. The level split mirrors the hono adapter:
276
+ * 5xx is ours to fix and gets a stack, 4xx is the caller's and would otherwise let a bot
277
+ * scanning for /wp-admin flood the error stream.
278
+ */
279
+ private logHandledError;
280
+ /**
281
+ * Produces the response for a thrown error, in the same order the hono adapter uses.
282
+ *
283
+ * The application's handler gets first refusal on *every* error, `HttpException` included.
284
+ * Previously each catch block answered an `HttpException` straight from `getResponse()` and
285
+ * only consulted `errorHandler` for everything else, so an app could not reshape its own
286
+ * 401/403/404 envelopes. Worse, only one of the five catch blocks carried the
287
+ * `!isValidationError` exemption, so a `ValidationError` thrown on the fast path never
288
+ * reached `onError` at all - the exact opposite of why ValidationError exists.
289
+ *
290
+ * Falls back to the exception's own response (or a 500) when there is no handler, when the
291
+ * handler returns nothing, or when the handler itself throws.
292
+ */
293
+ private respondToError;
226
294
  /**
227
295
  * Executes middleware chain with recursive next() pattern
228
296
  *
@@ -342,7 +410,6 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
342
410
  * - Error handling
343
411
  *
344
412
  * @param route - Route parameters
345
- * @param _commonMiddlewares - Common middlewares for this route group (for future optimization)
346
413
  * @returns Bun-compatible route handler
347
414
  */
348
415
  private createRouteHandler;
@@ -438,66 +505,6 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
438
505
  * ```
439
506
  */
440
507
  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
508
  get hostname(): any;
502
509
  set hostname(value: any);
503
510
  /**