@dunx/http 2.5.0 → 3.0.1

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.
@@ -1,14 +1,9 @@
1
1
  import { AppError, type Ctor, type Logger } from '@dunx/core';
2
2
  export interface HttpErrorOptions extends ErrorOptions {
3
3
  /**
4
- * Headers the error response carries. `Retry-After` on a 429,
5
- * `WWW-Authenticate` on a 401, `Allow` on a 405 - each of them part of the
6
- * status rather than an extra, and none of them expressible by a throw before
7
- * this existed.
8
- *
9
- * {@link errorMapper} copies them onto the response. An app that replaces the
10
- * mapper has to read them itself, which is the same contract `status` and
11
- * `message` already have.
4
+ * Headers the error response carries: `Retry-After` on a 429,
5
+ * `WWW-Authenticate` on a 401, `Allow` on a 405. {@link errorMapper} copies them
6
+ * onto the response; an app replacing the mapper reads them itself.
12
7
  */
13
8
  readonly headers?: Readonly<Record<string, string>>;
14
9
  }
@@ -37,16 +32,9 @@ export declare class ValidationError extends HttpError {
37
32
  }
38
33
  export type ErrorMapper = (error: unknown, req: Request) => Response;
39
34
  /**
40
- * The class form of {@link ErrorMapper}, and the one to reach for in an app.
41
- *
42
- * A mapper is a function, which means it cannot inject: the interesting ones need
43
- * the app's config to decide how much of an error to reveal, or its `Logger` to
44
- * record the ones that became a 500. dunx's own default proves the point - it is
45
- * `errorMapper(logger)`, a curried factory, because currying was the only way to
46
- * hand a function a dependency.
47
- *
48
- * A filter is resolved **from the container**, exactly as `HttpOptions.middleware`
49
- * entries are, so it takes whatever it needs as constructor parameters:
35
+ * The class form of {@link ErrorMapper}, and the one to reach for in an app. A
36
+ * mapper is a function and cannot inject; a filter is resolved from the container
37
+ * exactly as `HttpOptions.middleware` entries are:
50
38
  *
51
39
  * ```ts
52
40
  * export class AppErrorFilter extends ErrorFilter {
@@ -66,60 +54,41 @@ export type ErrorMapper = (error: unknown, req: Request) => Response;
66
54
  * HttpFactory.create(root, { onError: AppErrorFilter });
67
55
  * ```
68
56
  *
69
- * `abstract class` rather than an interface, so it is a runtime value and therefore
70
- * usable as an injection token - an app that wants to swap filters by binding one
71
- * can. Extending it is optional: `onError` accepts any class with a matching
72
- * `catch`, because the check is structural.
73
- *
74
- * The method is `catch` to match the vocabulary of the thing it replaces, NestJS's
75
- * `ExceptionFilter.catch`. A filter that cannot handle an error should rethrow it,
76
- * or delegate to `defaultErrorMapper`.
57
+ * `abstract class` rather than an interface, so it is a runtime value and usable
58
+ * as an injection token. Extending it is optional: the check is structural. A
59
+ * filter that cannot handle an error should rethrow or delegate to
60
+ * `defaultErrorMapper`.
77
61
  */
78
62
  export declare abstract class ErrorFilter {
79
63
  abstract catch(error: unknown, req: Request): Response;
80
64
  }
81
- /**
82
- * What `onError` accepts. A bare mapper still works and is the cheaper thing for a
83
- * filter with no dependencies; a class is what an app that needs one uses.
84
- */
65
+ /** What `onError` accepts. A bare mapper is cheaper where nothing is injected. */
85
66
  export type ErrorHandler = ErrorMapper | Ctor<ErrorFilter>;
86
67
  /**
87
- * Whether `onError` was given a class rather than a mapper.
88
- *
89
- * Both are `typeof === 'function'`, so the discriminator is the prototype carrying
90
- * a `catch`: a class declaration always has one, and neither an arrow function nor
91
- * a `function` expression ever does. Checking `prototype` alone would be wrong -
92
- * `function mapper() {}` has an empty one.
68
+ * Whether `onError` was given a class rather than a mapper. Both are functions, so
69
+ * the discriminator is a prototype carrying a `catch`. `prototype` alone would be
70
+ * wrong: `function mapper() {}` has an empty one.
93
71
  */
94
72
  export declare const isErrorFilter: (handler: ErrorHandler) => handler is Ctor<ErrorFilter>;
95
73
  /**
96
- * Narrows an `ErrorHandler` to the mapper the request path actually calls.
97
- *
98
- * `resolve` is typed for this one token rather than generically: the only thing ever
99
- * looked up here is the filter, and a `<T>(token: Ctor<T>) => T` signature makes
100
- * every caller - a test included - satisfy a polymorphic contract it does not need.
74
+ * Narrows an `ErrorHandler` to the mapper the request path calls. `resolve` is
75
+ * typed for this one token rather than generically, so no caller has to satisfy a
76
+ * polymorphic contract it does not need.
101
77
  */
102
78
  export declare const toErrorMapper: (handler: ErrorHandler, resolve: (token: Ctor<ErrorFilter>) => ErrorFilter) => ErrorMapper;
103
79
  /**
104
80
  * The mapper `HttpFactory` installs unless `onError` replaces it, built from the
105
- * app's **bound** `Logger` - so a service that imported `@dunx/infra/logger` gets
106
- * the stack as one `@arkv/logger` entry, sanitized and shaped like every other.
107
- *
108
- * An `HttpError` is not logged here at all: the status is the whole record, and
109
- * `RequestLoggingMiddleware` already writes the 4xx line. Only an error nothing
110
- * declared - the one that becomes a 500 - is worth a stack.
81
+ * app's bound `Logger`. An `HttpError` is not logged here: the status is the whole
82
+ * record and `RequestLoggingMiddleware` already wrote the line, so only an
83
+ * undeclared error is worth a stack.
111
84
  *
112
- * The error goes in as its own argument rather than as a field of an object.
113
- * `JSON.stringify(new Error('x'))` is `{}`, so `{ err: error }` would drop the
114
- * stack; every `Logger` implementation picks an `Error` argument out and
115
- * serialises it.
85
+ * The error is its own argument, not a field: `JSON.stringify(new Error('x'))` is
86
+ * `{}`, so `{ err: error }` would drop the stack.
116
87
  */
117
88
  export declare const errorMapper: (logger: Logger) => ErrorMapper;
118
89
  /**
119
90
  * The same mapper with no container behind it, for `buildRoutes` and
120
- * `buildFallback` called directly. It writes through core's `ConsoleLogger`, which
121
- * is one JSON line - the point being that nothing in this package ever reaches for
122
- * `console.error` and emits a multi-line dump a collector reads as several broken
123
- * records. An app gets {@link errorMapper} over its own bound logger instead.
91
+ * `buildFallback` called directly. Writes through core's `ConsoleLogger`, so
92
+ * nothing here emits a multi-line dump a collector reads as several records.
124
93
  */
125
94
  export declare const defaultErrorMapper: ErrorMapper;
@@ -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,78 +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;
100
49
  /**
101
50
  * Adopt W3C Trace Context, so `traceId`, `spanId` and `parentSpanId` join
102
- * `requestId` on every line the request writes. Default **`false`**.
103
- *
104
- * On, an inbound `traceparent` is honoured and this service becomes a child
105
- * span of the caller's; off, nothing reads the header and nothing is minted.
106
- * It costs a header read and 8 random bytes per request, which is not worth
107
- * paying in a service with nothing to correlate against - and `requestId`
108
- * already spans two dunx services on its own.
109
- *
110
- * `@dunx/http/client` sends the adopted trace upstream, so turning this on at
111
- * both ends is what makes one trace cover both.
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.
112
54
  */
113
55
  readonly trace?: boolean;
114
56
  }
115
57
  /**
116
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.
117
61
  *
118
- * Installed by `HttpFactory.create` unless `requestLogging: false`. It injects
119
- * `Logger` and `RequestContext` - both `@dunx/core` contracts, both bound by
120
- * default - so it works with no logging module imported, and picks up
121
- * `@arkv/logger` automatically once `@dunx/infra/logger` is.
122
- *
123
- * **One entry, not two.** A framework whose middleware cannot see the response
124
- * needs a middleware for the inbound half and an
125
- * interceptor for the outbound one, because they are different classes and the
126
- * interceptor cannot see what the middleware saw. Here they are the same
127
- * closure, so there is no pair to correlate by `requestId` to find out how a
128
- * call ended. A 4xx is the same line at `warn`, a 5xx at `error`.
129
- *
130
- * Everything the handler logs in between carries `requestId`, `method`, `event`
131
- * and `context` without being passed anything, because the whole call runs
132
- * inside `runWithContext` - unless `correlate: false`, which drops the scope and
133
- * 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`.
134
65
  *
135
- * **Nothing here is `async`.** Reading the request or the response body are the
136
- * only steps that can ever wait, both are off by default, and both are adopted
137
- * with `.then` rather than awaited - the same rule `input.ts` follows, for the
138
- * same measured reason. An `async` scope callback alone cost 0.44 µs/request
139
- * 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.
140
68
  */
141
69
  export declare class RequestLoggingMiddleware implements Middleware {
142
70
  #private;
@@ -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;
@@ -27,17 +27,13 @@ export interface Trace {
27
27
  export declare class TraceContext {
28
28
  #private;
29
29
  /**
30
- * The inbound `traceparent`, or a fresh trace.
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.
31
34
  *
32
- * A malformed header is discarded rather than repaired, which is what the
33
- * standard requires: an unparseable `traceparent` means the caller's trace is
34
- * unknown, not that this request has none. Version `ff` is invalid, and a
35
- * higher version keeps its first four fields and drops the rest, so a future
36
- * format still propagates through this service instead of being dropped.
37
- *
38
- * When nothing arrives, `traceId` is the request id with its hyphens removed -
39
- * a UUID is 16 bytes, which is exactly a trace id, and reusing it means one
40
- * identifier in two spellings rather than a second `crypto` call per request.
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.
41
37
  */
42
38
  static adopt(req: Request, requestId: string): Trace;
43
39
  /** The trace adopted for this request, if one was. */
@@ -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;