@dunx/http 2.4.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.
Files changed (40) hide show
  1. package/README.md +31 -909
  2. package/dist/chunk-25g22350.js +58 -0
  3. package/dist/chunk-ywdpxbkf.js +1573 -0
  4. package/dist/client/module.d.ts +2 -7
  5. package/dist/client/options.d.ts +12 -0
  6. package/dist/client/service.d.ts +11 -22
  7. package/dist/client.d.ts +8 -2
  8. package/dist/client.js +26 -137
  9. package/dist/compression/compression.d.ts +25 -0
  10. package/dist/compression/module.d.ts +20 -0
  11. package/dist/compression/negotiate.d.ts +12 -0
  12. package/dist/compression/options.d.ts +61 -0
  13. package/dist/health/indicators.d.ts +5 -10
  14. package/dist/index.d.ts +24 -20
  15. package/dist/index.js +325 -1366
  16. package/dist/internal.d.ts +36 -0
  17. package/dist/internal.js +89 -0
  18. package/dist/route/decorators.d.ts +6 -6
  19. package/dist/route/marker.d.ts +13 -0
  20. package/dist/route/metadata.d.ts +6 -9
  21. package/dist/route/schema.d.ts +59 -29
  22. package/dist/server/application.d.ts +33 -89
  23. package/dist/server/client-address.d.ts +6 -13
  24. package/dist/server/errors.d.ts +24 -55
  25. package/dist/server/request-id.d.ts +6 -11
  26. package/dist/server/request-logging.d.ts +33 -91
  27. package/dist/server/routes.d.ts +8 -20
  28. package/dist/server/trace-context.d.ts +47 -0
  29. package/dist/static/files.d.ts +5 -13
  30. package/dist/static/module.d.ts +10 -19
  31. package/dist/throttle/guard.d.ts +6 -9
  32. package/dist/throttle/module.d.ts +6 -8
  33. package/dist/throttle/store.d.ts +11 -20
  34. package/dist/ws/middleware.d.ts +12 -21
  35. package/dist/ws/redis-relay.d.ts +9 -19
  36. package/package.json +7 -3
  37. package/dist/chunk-sz4pvqxy.js +0 -111
  38. package/dist/chunk-sz4pvqxy.js.map +0 -10
  39. package/dist/client.js.map +0 -15
  40. package/dist/index.js.map +0 -53
@@ -2,18 +2,13 @@ export declare const REQUEST_ID_HEADER = "x-request-id";
2
2
  /**
3
3
  * The request id, and the only thing that decides a request has one.
4
4
  *
5
- * `RequestLoggingMiddleware` sets the header on a response it returns, and a
6
- * failure is never one: `buildRoutes` and `buildFallback` catch outside the chain
7
- * and build a fresh `Response` from the error mapper. So a guard's 401, a
8
- * validation 400, a mapped 500 and every unmatched 404 went out with no id on
9
- * them, which are the responses a caller most needs in order to find the log line
10
- * the middleware just wrote.
5
+ * The logging middleware sets the header on a response it returns, and a failure
6
+ * is never one - the error mapper builds a fresh `Response` outside the chain. So
7
+ * a guard's 401, a validation 400 and every unmatched 404 went out with no id.
11
8
  *
12
- * Recorded against the request rather than threaded through the mapper, because
13
- * `ErrorMapper` is `(error, req) => Response` and an app writes its own.
14
- * {@link stamp} then reads back whatever {@link assign} recorded, so an app that
15
- * turned request logging off, or a path it told the middleware to ignore, is still
16
- * answered without a header: nothing minted an id, so there is none to stamp.
9
+ * Recorded against the request rather than threaded through the mapper, which an
10
+ * app writes its own of. {@link stamp} reads back what {@link assign} recorded, so
11
+ * a path nothing minted an id for is still answered without a header.
17
12
  */
18
13
  export declare class RequestIds {
19
14
  /**
@@ -6,57 +6,26 @@ export interface RequestLoggingOptions {
6
6
  /** Bodies past this many characters are logged as a size. Default 2048. `0` omits them. */
7
7
  readonly maxBodyLength?: number;
8
8
  /**
9
- * Log the request body. Default **`false`**.
9
+ * Log the request body. Default `false`, and the cost depends on whether the
10
+ * route declares a `body` schema: +1.9 us when it does, +28.8 us when it does
11
+ * not, because the logger has to `req.clone()` an unread network stream.
10
12
  *
11
- * **What it costs depends on whether the route declares a `body` schema**, and by
12
- * a factor of fifteen. `bun run logging:bodies` in `internal/bench`, round-robin
13
- * over 3 runs against `POST /validate`:
14
- *
15
- * | setting | us/req | vs the default |
16
- * | ---------------------------------------- | -----: | -------------: |
17
- * | `requestLogging: false` | 12.80 | -4.45 us |
18
- * | the shipped default, both bodies off | 17.25 | - |
19
- * | **`requestBody: true`, schema route** | **19.12** | **+1.87 us** |
20
- * | `responseBody: true` | 19.80 | +2.55 us |
21
- * | both bodies, schema route | 20.03 | +2.78 us |
22
- * | **`requestBody: true`, no schema** | **46.06** | **+28.81 us** |
23
- *
24
- * A route with a schema has already had its body buffered by the input reader, so
25
- * the logger reads that text and nothing is cloned. A route without one leaves the
26
- * logger to `req.clone()`, and cloning a request whose body is an unread network
27
- * stream is the entire cost - not the second `JSON.parse`, which is 0.32 us.
28
- * `raw-body.ts` has that decomposition.
29
- *
30
- * It is the field most likely to contain a password. Turn it on in development,
31
- * where seeing the payload is the point.
13
+ * It is the field most likely to contain a password.
32
14
  */
33
15
  readonly requestBody?: boolean;
34
- /**
35
- * Log the response body. Default **`false`**, +2.55 us - see the table above.
36
- *
37
- * No equivalent trick here and none needed: a response is already a materialised
38
- * string by the time this clones it, which is why it was never the expensive half.
39
- */
16
+ /** Log the response body. Default `false`, +2.6 us. A response is already a
17
+ * materialised string by the time this clones it. */
40
18
  readonly responseBody?: boolean;
41
19
  /**
42
- * Paths to skip entirely - a health check polled every second, say.
43
- *
44
- * **Entirely** is literal: no entry, no `x-request-id` on the response, and no
45
- * `AsyncLocalStorage` scope, so anything the handler logs is uncorrelated. That
46
- * is what makes it free. `correlateIgnored` buys the correlation back.
20
+ * Paths to skip entirely: no entry, no `x-request-id`, and no
21
+ * `AsyncLocalStorage` scope, so anything the handler logs is uncorrelated.
22
+ * `correlateIgnored` buys the correlation back.
47
23
  */
48
24
  readonly ignore?: readonly string[];
49
25
  /**
50
- * Path **prefixes** to skip, for a whole mount rather than one path.
51
- *
52
- * `ignore` is an exact-match `Set` because that is one lookup on the hot path
53
- * and a health check is one path. A mount is not: `@dunx/dashboard` at
54
- * `/_dunx` polls four endpoints every five seconds and bull-board pulls a
55
- * dozen assets, and listing them is both tedious and wrong the moment either
56
- * grows an endpoint.
57
- *
58
- * Scanned only when non-empty, so an app that sets none pays nothing - the
59
- * same guard `ignore` has. Keep the list short; it is a loop.
26
+ * Path prefixes to skip, for a whole mount rather than one path. `ignore` is an
27
+ * exact-match `Set`; this is a loop, so keep the list short. Scanned only when
28
+ * non-empty.
60
29
  *
61
30
  * ```ts
62
31
  * requestLogging: { ignorePrefix: ['/_dunx'] }
@@ -65,64 +34,37 @@ export interface RequestLoggingOptions {
65
34
  readonly ignorePrefix?: readonly string[];
66
35
  /**
67
36
  * Keep the request id and the async scope on an `ignore`d path. Default
68
- * **`false`**.
69
- *
70
- * "Do not log the health check, but do keep its request id" is this. The path
71
- * still writes no entry of its own; it gets an id - inbound or minted - on the
72
- * response, and everything the handler logs carries it.
73
- *
74
- * It is not the default because it is not free: the ignored path pays for
75
- * reading the header, `crypto.randomUUID()`, the `runWithContext` scope and the
76
- * response header. On the `bun run logging` decomposition those four rows are
77
- * ~2.2 µs, against ~5.4 µs for the whole default path - so it costs the half
78
- * that buys correlation and not the half that builds and serialises the entry.
37
+ * `false`. The path still writes no entry; it gets an id on the response and
38
+ * everything the handler logs carries it. Costs ~2.2 us of the ~5.4 us the
39
+ * default path spends.
79
40
  */
80
41
  readonly correlateIgnored?: boolean;
81
42
  /**
82
- * Wrap every request in an `AsyncLocalStorage` scope. Default **`true`**.
83
- *
84
- * The scope is what lets a service logging four frames down come out carrying
85
- * `requestId` without being handed a request object. It is measured: the
86
- * `runWithContext` row of `bun run logging` is **+0.91 µs**, 17% of the 5.38 µs
87
- * request logging costs over `requestLogging: false`.
88
- *
89
- * `correlate: false` skips it. **The request entry is unchanged** - the same
90
- * `requestId`, `method`, `event`, `flow` and `context` fields are written onto
91
- * it directly instead of being read back out of the store. What is lost is
92
- * everything *else* the request logs: those lines carry no `requestId`, and
93
- * `updateContext` from a handler has nothing to update.
94
- *
95
- * Worth it for an app whose handlers never log, or one that passes correlation
96
- * explicitly. Leave it on otherwise; correlation is most of what a request id
97
- * is for.
43
+ * Wrap every request in an `AsyncLocalStorage` scope. Default `true`, +0.91 us.
44
+ * It is what lets a service four frames down log `requestId` without being
45
+ * handed a request. `correlate: false` skips it; this middleware's own entry is
46
+ * unchanged, but every other line the request writes loses its id.
98
47
  */
99
48
  readonly correlate?: boolean;
49
+ /**
50
+ * Adopt W3C Trace Context, so `traceId`, `spanId` and `parentSpanId` join
51
+ * `requestId`. Default `false`: it costs a header read and 8 random bytes, and
52
+ * `requestId` already spans two dunx services. `@dunx/http/client` sends the
53
+ * adopted trace upstream.
54
+ */
55
+ readonly trace?: boolean;
100
56
  }
101
57
  /**
102
58
  * One structured entry per request, carrying the request and its response.
59
+ * Installed by `HttpFactory.create` unless `requestLogging: false`, and injecting
60
+ * only core contracts, so it works with no logging module imported.
103
61
  *
104
- * Installed by `HttpFactory.create` unless `requestLogging: false`. It injects
105
- * `Logger` and `RequestContext` - both `@dunx/core` contracts, both bound by
106
- * default - so it works with no logging module imported, and picks up
107
- * `@arkv/logger` automatically once `@dunx/infra/logger` is.
108
- *
109
- * **One entry, not two.** A framework whose middleware cannot see the response
110
- * needs a middleware for the inbound half and an
111
- * interceptor for the outbound one, because they are different classes and the
112
- * interceptor cannot see what the middleware saw. Here they are the same
113
- * closure, so there is no pair to correlate by `requestId` to find out how a
114
- * call ended. A 4xx is the same line at `warn`, a 5xx at `error`.
115
- *
116
- * Everything the handler logs in between carries `requestId`, `method`, `event`
117
- * and `context` without being passed anything, because the whole call runs
118
- * inside `runWithContext` - unless `correlate: false`, which drops the scope and
119
- * with it that guarantee, but not the fields on this middleware's own entry.
62
+ * One entry rather than a middleware and an interceptor to correlate: middleware
63
+ * wraps `next()`, so both halves are the same closure. A 4xx logs at `warn`, a
64
+ * 5xx at `error`.
120
65
  *
121
- * **Nothing here is `async`.** Reading the request or the response body are the
122
- * only steps that can ever wait, both are off by default, and both are adopted
123
- * with `.then` rather than awaited - the same rule `input.ts` follows, for the
124
- * same measured reason. An `async` scope callback alone cost 0.44 µs/request
125
- * against a synchronous one on raw `Bun.serve`.
66
+ * Nothing here is `async`. The two steps that can wait are off by default and
67
+ * adopted with `.then`; an `async` scope callback alone cost 0.44 us/request.
126
68
  */
127
69
  export declare class RequestLoggingMiddleware implements Middleware {
128
70
  #private;
@@ -1,6 +1,6 @@
1
1
  import { type Ctor, type ModuleRef } from '@dunx/core';
2
2
  import type { DiscoveredRoute } from '../route/discover.js';
3
- import type { HttpMethod } from '../route/marker.js';
3
+ import { type HttpMethod } from '../route/marker.js';
4
4
  import type { UpgradeHandler } from '../ws/adapter.js';
5
5
  import { type CorsOptions } from './cors.js';
6
6
  import { type ErrorMapper } from './errors.js';
@@ -39,19 +39,12 @@ export declare const assertNoGatewayCollisions: (discovered: readonly Discovered
39
39
  */
40
40
  export declare const withUpgradeRoutes: (routes: BunRoutes, gateways: ReadonlyMap<string, UpgradeHandler>) => ServeRoutes;
41
41
  /**
42
- * Bun answers an unmatched path itself, so nothing in the middleware chain ever
43
- * sees it - which makes a 404 invisible to request logging, metrics and tracing.
42
+ * Bun answers an unmatched path itself, so nothing in the middleware chain sees
43
+ * it and a 404 is invisible to request logging. This is the only `fetch` handler
44
+ * dunx installs and it is not a router: it runs once Bun has matched nothing.
44
45
  *
45
- * This is the only `fetch` handler dunx installs, and it is not a router: Bun
46
- * still does all the matching, and this runs only once Bun has decided nothing
47
- * matched. It puts the global middleware in front of a 404 in the framework's
48
- * own error shape.
49
- *
50
- * **The miss is a `throw`, not a returned `Response`.** `miss` raises
51
- * `HttpError(404)` and `compose` propagates it, so a middleware written as
52
- * `const response = await next(); if (response.status === 404) ...` never reaches
53
- * its own second line on an unmatched path - the rewrite it was written for is the
54
- * one case it cannot see. A middleware that means to act on a miss has to catch:
46
+ * The miss is a throw, not a returned `Response`, so a middleware reading
47
+ * `(await next()).status` never sees one and has to catch:
55
48
  *
56
49
  * ```ts
57
50
  * try {
@@ -62,13 +55,8 @@ export declare const withUpgradeRoutes: (routes: BunRoutes, gateways: ReadonlyMa
62
55
  * }
63
56
  * ```
64
57
  *
65
- * `ctx.get(UNMATCHED)` is the other half, and the cheaper one: it is set here and
66
- * by no real route, so a middleware can tell "nothing matched this path" from "a
67
- * handler answered 404 for a record that does not exist" **before** calling
68
- * `next()` at all. Only the second of those is a `Response` to inspect.
69
- *
70
- * Composed per request rather than at boot, because the context names the path
71
- * that missed. That allocation is on the 404 path only.
58
+ * `ctx.get(UNMATCHED)` is the cheaper half: set here and by no real route, so a
59
+ * middleware can tell a miss from a handler's own 404 before calling `next()`.
72
60
  */
73
61
  export declare const buildFallback: (middleware?: readonly Middleware[], onError?: ErrorMapper, cors?: CorsOptions, notFound?: 'guarded' | 'public') => RouteHandler;
74
62
  export declare const buildRoutes: (discovered: readonly DiscoveredRoute[], middleware?: readonly Middleware[], onError?: ErrorMapper, cors?: CorsOptions, resolve?: GuardResolver) => BunRoutes;
@@ -0,0 +1,47 @@
1
+ export declare const TRACEPARENT_HEADER = "traceparent";
2
+ export declare const TRACESTATE_HEADER = "tracestate";
3
+ export interface Trace {
4
+ /** 32 hex digits, shared by every span in the trace. */
5
+ readonly traceId: string;
6
+ /** 16 hex digits identifying this server's work on this request. */
7
+ readonly spanId: string;
8
+ /** The caller's span, when one arrived in `traceparent`. */
9
+ readonly parentSpanId?: string;
10
+ /** Two hex digits. Bit 0 is `sampled`. */
11
+ readonly flags: string;
12
+ /** `tracestate` verbatim, when one arrived. Vendor data this server does not read. */
13
+ readonly state?: string;
14
+ }
15
+ /**
16
+ * W3C Trace Context, propagated across services.
17
+ *
18
+ * The whole of it is one header parsed and one header written. There is no
19
+ * exporter, no sampler and no dependency: what this buys is that every log line a
20
+ * request writes carries the same `traceId` the service upstream logged, so the
21
+ * two can be joined without either of them running a collector.
22
+ *
23
+ * `@dunx/http` does not turn this on by itself - `requestLogging: { trace: true }`
24
+ * does. Adopting a trace costs a header read and 8 random bytes on every request,
25
+ * which is not worth spending in a service that has nothing to correlate with.
26
+ */
27
+ export declare class TraceContext {
28
+ #private;
29
+ /**
30
+ * The inbound `traceparent`, or a fresh trace. A malformed header is discarded
31
+ * rather than repaired, as the standard requires. Version `ff` is invalid; a
32
+ * higher version keeps its first four fields, so a future format still
33
+ * propagates.
34
+ *
35
+ * With nothing inbound, `traceId` is the request id minus its hyphens - a UUID
36
+ * is 16 bytes, exactly a trace id, so there is no second `crypto` call.
37
+ */
38
+ static adopt(req: Request, requestId: string): Trace;
39
+ /** The trace adopted for this request, if one was. */
40
+ static of(req: Request): Trace | undefined;
41
+ /**
42
+ * The `traceparent` to send upstream. This server's span becomes the callee's
43
+ * parent, so the two link without inventing a span nothing logged.
44
+ */
45
+ static header(trace: Pick<Trace, 'traceId' | 'spanId' | 'flags'>): string;
46
+ static sampled(trace: Pick<Trace, 'flags'>): boolean;
47
+ }
@@ -3,20 +3,12 @@ import type { Middleware, Next } from '../server/middleware.js';
3
3
  import type { RouteContext } from '../server/context.js';
4
4
  import { StaticOptions } from './options.js';
5
5
  /**
6
- * Static files, on `Bun.file`.
6
+ * Static files, on `Bun.file`. A `Bun.file` handed to a `Response` already
7
+ * streams, sets `content-type`, answers a `Range` request and uses `sendfile(2)`,
8
+ * so this file is a path check and a cache policy.
7
9
  *
8
- * Nest has `ServeStaticModule` over `serve-static`, which is Express middleware
9
- * doing its own `stat`, its own range parsing, its own ETag and its own MIME table.
10
- * None of that is needed here: `Bun.file(path)` handed to a `Response` already
11
- * streams, already sets `content-type` from the extension, already answers a
12
- * `Range` request, and does the whole thing with `sendfile(2)` rather than reading
13
- * into JavaScript. So this file is a **path check and a cache policy**, and that is
14
- * the entire justification for it existing.
15
- *
16
- * A middleware rather than routes, for the same reason the dashboard is one: the
17
- * file set is whatever is on disk at request time, and turning it into a
18
- * `Bun.serve` route table would mean walking a directory at boot and being wrong
19
- * the moment anything changed.
10
+ * A middleware rather than routes: the file set is whatever is on disk at request
11
+ * time, and a route table would be walked at boot and wrong thereafter.
20
12
  */
21
13
  export declare class StaticFiles implements Middleware {
22
14
  #private;
@@ -1,26 +1,21 @@
1
1
  import { type Deps, type DynamicModule, type FactoryProvider } from '@dunx/core';
2
2
  import { type StaticOptionsInit } from './options.js';
3
3
  /**
4
- * Serves a directory, the way Nest's `ServeStaticModule` does - and like the
5
- * dashboard, **it does not register itself**. The app does:
4
+ * Serves a directory. Like the dashboard, it does not register itself:
6
5
  *
7
6
  * ```ts
8
7
  * const app = await HttpFactory.create(AppModule);
9
8
  * app.use(StaticFiles);
10
9
  * ```
11
10
  *
12
- * Position in the chain is the decision being left to the app. Static assets
13
- * usually want to be *outside* an auth guard and *inside* request logging, and no
14
- * default can know which. Anything outside the mount falls through untouched, so
15
- * the app's own routes and its 404 behave exactly as before.
11
+ * Position in the chain is left to the app - assets usually want to be outside an
12
+ * auth guard and inside request logging. Anything outside the mount falls through
13
+ * untouched.
16
14
  *
17
- * There is no `index.html` fallback and no SPA rewrite: building them in would mean
18
- * this middleware deciding what a 404 means for paths it does not own.
19
- *
20
- * An app that wants one writes a middleware **outside** this one, and the shape
21
- * matters. An unmatched path is a **thrown** `HttpError(404)`, not a returned
22
- * `Response` - see `buildFallback` - so reading `(await next()).status` never sees a
23
- * miss, and `ctx.get(UNMATCHED)` is what does:
15
+ * There is no `index.html` fallback and no SPA rewrite, which would mean this
16
+ * middleware deciding what a 404 means for paths it does not own. An app that
17
+ * wants one writes a middleware outside this one, reading `ctx.get(UNMATCHED)`
18
+ * rather than a returned status, since a miss is thrown:
24
19
  *
25
20
  * ```ts
26
21
  * export class SpaFallback implements Middleware {
@@ -45,12 +40,8 @@ import { type StaticOptionsInit } from './options.js';
45
40
  * }
46
41
  * ```
47
42
  *
48
- * Two more things that shape where it goes in the chain. `notFound: 'guarded'` -
49
- * the default - reports a miss with no route metadata, so a global session guard
50
- * refuses it and the status is a 401 rather than a 404; an app serving a SPA wants
51
- * `notFound: 'public'`. And the fallback answers **before** any middleware listed
52
- * after the guard, so the rewrite has to sit ahead of the guard to see the miss at
53
- * all.
43
+ * `notFound: 'guarded'`, the default, reports a miss with no route metadata, so a
44
+ * session guard refuses it with a 401; a SPA wants `notFound: 'public'`.
54
45
  */
55
46
  export declare class StaticModule {
56
47
  static forRoot(init: StaticOptionsInit): DynamicModule;
@@ -8,17 +8,14 @@ import { ThrottleStore } from './store.js';
8
8
  /**
9
9
  * A fixed-window rate limit, one key per subject and handler.
10
10
  *
11
- * **Fails open.** A store that cannot be reached allows the request and warns
12
- * **once per process** - a line per request would be its own outage, and refusing
13
- * every request because the counter is down turns a degraded dependency into a
14
- * dead service.
11
+ * Fails open: an unreachable store allows the request and warns once per process,
12
+ * since refusing everything because the counter is down turns a degraded
13
+ * dependency into a dead service.
15
14
  *
16
- * **List it after any session guard.** An authenticated caller should be limited by
17
- * user id and an anonymous one by address, and only the guard ahead of this one
18
- * knows which - which is what `ThrottleOptions.subject` reads.
15
+ * List it after any session guard - only the guard ahead knows whether to limit by
16
+ * user id or by address, which is what `ThrottleOptions.subject` reads.
19
17
  *
20
- * The 429 is thrown, never returned, so it goes through the app's own `onError` and
21
- * comes out in the app's error shape like every other status.
18
+ * The 429 is thrown, so it comes out in the app's own error shape.
22
19
  */
23
20
  export declare class ThrottleGuard implements Middleware {
24
21
  #private;
@@ -1,12 +1,11 @@
1
1
  import { type Deps, type DynamicModule, type FactoryProvider } from '@dunx/core';
2
2
  import { type ThrottleOptionsInit } from './options.js';
3
3
  /**
4
- * A first-class rate limit: the decorator, the guard, the counter and its options.
4
+ * The decorator, the guard, the counter and its options.
5
5
  *
6
- * `global: true`, because the guard is listed in `HttpOptions.middleware` - which
7
- * is the app's own list, resolved from wherever the class is declared - and a
8
- * non-global module would make every consumer import this one to reach a guard it
9
- * never names.
6
+ * `global: true`: the guard is listed in `HttpOptions.middleware`, the app's own
7
+ * list, so a non-global module would make every consumer import this one to reach
8
+ * a guard it never names.
10
9
  *
11
10
  * ```ts
12
11
  * ThrottleModule.forRootAsync({
@@ -22,9 +21,8 @@ import { type ThrottleOptionsInit } from './options.js';
22
21
  * HttpFactory.create(AppModule, { middleware: [SessionGuard, ThrottleGuard] });
23
22
  * ```
24
23
  *
25
- * Position in the chain is the app's, the same decision `StaticFiles` leaves open,
26
- * and for a sharper reason: ahead of a session guard the limit counts every caller
27
- * as an address.
24
+ * Position in the chain is the app's: ahead of a session guard, the limit counts
25
+ * every caller as an address.
28
26
  */
29
27
  export declare class ThrottleModule {
30
28
  static forRoot(init: ThrottleOptionsInit): DynamicModule;
@@ -1,15 +1,10 @@
1
1
  /**
2
- * The counter behind the guard.
2
+ * The counter behind the guard. An `abstract class` rather than an interface,
3
+ * since an interface at an injection site is a boot error.
3
4
  *
4
- * An `abstract class` rather than an interface: `@dunx/transform` records
5
- * constructor parameter *types*, so an interface at an injection site is a boot
6
- * error. Same reason `RedisConnection` and `Logger` are classes.
7
- *
8
- * **A fixed window, not a sliding one.** `hit` increments and returns the count for
9
- * the window the key is already in; the window starts at the first hit and ends
10
- * when the key expires. A sliding window needs a sorted set per subject and a
11
- * range trim per request, which is a different cost for an accuracy a rate limit
12
- * does not need.
5
+ * A fixed window: `hit` returns the count for the window the key is already in,
6
+ * which starts at the first hit and ends when the key expires. A sliding window
7
+ * needs a sorted set per subject and a trim per request.
13
8
  */
14
9
  export declare abstract class ThrottleStore {
15
10
  constructor();
@@ -51,17 +46,13 @@ export declare class RedisThrottleStore extends ThrottleStore {
51
46
  ttl(key: string): Promise<number | undefined>;
52
47
  }
53
48
  /**
54
- * The single-process counter, and the default - so an app with no Redis still
55
- * limits something rather than nothing.
56
- *
57
- * It is per process, which is the whole caveat: two replicas each allow the full
58
- * budget. `RedisThrottleStore` is the answer for more than one.
49
+ * The single-process counter and the default, so an app with no Redis still limits
50
+ * something. Per process is the caveat: two replicas each allow the full budget,
51
+ * and `RedisThrottleStore` is the answer for more than one.
59
52
  *
60
- * The map is bounded. An expired entry is dropped when its key is next touched,
61
- * and once the map passes `maxKeys` every expired entry is swept - so a burst
62
- * across many subjects cannot grow it without limit. Reaching the cap with nothing
63
- * expired clears it, which resets a window early rather than holding memory a
64
- * server does not have.
53
+ * The map is bounded. Expired entries are dropped on touch and swept past
54
+ * `maxKeys`; reaching the cap with nothing expired clears it, resetting a window
55
+ * early rather than holding memory.
65
56
  */
66
57
  export declare class MemoryThrottleStore extends ThrottleStore {
67
58
  #private;
@@ -43,35 +43,26 @@ export interface SocketFrame {
43
43
  */
44
44
  export type SocketNext = () => unknown;
45
45
  /**
46
- * The single extension point on the socket side, shaped like {@link Middleware} on
47
- * the HTTP side: one method, wrapping `next()`.
46
+ * The socket side's single extension point, shaped like {@link Middleware}: one
47
+ * method wrapping `next()`. It sees every dispatched handler, and open and close
48
+ * arrive even for a gateway declaring neither.
48
49
  *
49
- * It sees every dispatched handler - open, each named message, the catch-all,
50
- * close, drain, ping and pong - and open and close arrive even for a gateway that
51
- * declares no `@OnOpen`/`@OnClose`, so a connection is never invisible to it.
50
+ * A throwing handler passes through here. Rethrow to leave the outcome to
51
+ * `SocketOptions.onError`, or return a value to answer the frame.
52
52
  *
53
- * A throwing or rejecting handler passes through here, which is where a guard
54
- * refuses and where an observer records the failure. Rethrow to leave the outcome
55
- * to `SocketOptions.onError`; return a value instead to answer the frame.
56
- *
57
- * Three things it cannot see, because they never reach the dispatcher: a
58
- * `socket.send` a handler makes itself, a `PubSub` broadcast, and the upgrade -
59
- * which is an HTTP request answered by the gateway's own route.
53
+ * It cannot see a `socket.send` a handler makes itself, a `PubSub` broadcast, or
54
+ * the upgrade, which is an HTTP request.
60
55
  */
61
56
  export interface SocketMiddleware {
62
57
  /**
63
- * That a failure passing through here is reported somewhere. Default
64
- * **`false`**.
58
+ * That a failure passing through here is reported somewhere. Default `false`.
65
59
  *
66
60
  * `SocketOptions.onError`'s `console.error` fallback is not installed while any
67
- * socket middleware exists, because a middleware wraps the handler and would
68
- * report the same failure a second time. Whether it does is something only the
69
- * middleware knows: one that ignores a throw turns error reporting off for the
70
- * whole server, and nothing about the wiring says so.
61
+ * socket middleware exists, since a middleware would report the same failure
62
+ * twice. Only the middleware knows whether it does, and one that ignores a throw
63
+ * would silently turn error reporting off for the whole server.
71
64
  *
72
- * Setting it is how a middleware says it does report. Leaving it unset with no
73
- * `websocket.onError` beside it is what `HttpFactory.create` warns about at
74
- * boot.
65
+ * Unset with no `websocket.onError` beside it is what `create` warns about.
75
66
  */
76
67
  readonly reportsErrors?: boolean;
77
68
  handle(frame: SocketFrame, ctx: SocketContext, next: SocketNext): unknown;
@@ -5,15 +5,10 @@ export interface RedisRelayOptions {
5
5
  /** @default `$VALKEY_URL`, `$REDIS_URL`, then `redis://localhost:6379` */
6
6
  readonly url?: string;
7
7
  /**
8
- * Bun's reconnection budget.
9
- *
10
- * `0` by default, and that default is not a preference: a `Bun.RedisClient` that
11
- * never connects keeps an internal retry timer alive past `close()`, and the
12
- * process then never exits. A relay is exactly the connection most likely to be
13
- * absent - a single-node deployment with `REDIS_URL` left over from staging -
14
- * so the default has to be the one that lets the app boot, degrade, and still
15
- * exit. Raise it when Redis is a hard requirement and you want Bun to reconnect
16
- * for you.
8
+ * Bun's reconnection budget. `0` by default: a `Bun.RedisClient` that never
9
+ * connects keeps a retry timer alive past `close()`, so the process never exits,
10
+ * and a relay is the connection most likely to be absent. Raise it where Redis
11
+ * is a hard requirement.
17
12
  *
18
13
  * @default 0
19
14
  */
@@ -23,17 +18,12 @@ export interface RedisRelayOptions {
23
18
  readonly tls?: boolean | Bun.TLSOptions;
24
19
  }
25
20
  /**
26
- * A {@link PubSubRelay} on `Bun.RedisClient` - a Bun global, so this costs
27
- * `@dunx/http` no dependency at all.
28
- *
29
- * **Two connections, not one.** A client in subscriber mode rejects every data
30
- * command, and throws synchronously doing it, so the subscription cannot share the
31
- * socket that publishes. This is the same split the socket.io Redis adapter makes
32
- * with its `pubClient` / `subClient`.
21
+ * A {@link PubSubRelay} on `Bun.RedisClient`, a Bun global, so it costs
22
+ * `@dunx/http` no dependency.
33
23
  *
34
- * Both are opened lazily, on the first call that needs them, and a failed one is
35
- * discarded so the next call builds a fresh connection rather than reusing a dead
36
- * one.
24
+ * Two connections: a client in subscriber mode rejects every data command, so the
25
+ * subscription cannot share the publishing socket. Both open lazily, and a failed
26
+ * one is discarded rather than reused.
37
27
  */
38
28
  export declare class RedisRelay implements PubSubRelay {
39
29
  #private;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "2.4.0",
3
+ "version": "3.0.0",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -40,6 +40,10 @@
40
40
  "./client": {
41
41
  "types": "./dist/client.d.ts",
42
42
  "import": "./dist/client.js"
43
+ },
44
+ "./internal": {
45
+ "types": "./dist/internal.d.ts",
46
+ "import": "./dist/internal.js"
43
47
  }
44
48
  },
45
49
  "publishConfig": {
@@ -58,7 +62,7 @@
58
62
  "@dunx/core": "workspace:*"
59
63
  },
60
64
  "peerDependencies": {
61
- "@dunx/core": "^2.4.0",
65
+ "@dunx/core": "^3.0.0",
62
66
  "@types/bun": ">=1.3.0"
63
67
  },
64
68
  "peerDependenciesMeta": {
@@ -67,6 +71,6 @@
67
71
  }
68
72
  },
69
73
  "engines": {
70
- "bun": ">=1.3.0"
74
+ "bun": ">=1.4.0"
71
75
  }
72
76
  }