@zudojs/middleware 0.0.1 → 1.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 +163 -16
- package/dist/middlewareCore/index.d.ts +1 -1
- package/dist/middlewareCore/index.js +1 -1
- package/dist/middlewareCore/middlewareCore.compose.d.ts +36 -4
- package/dist/middlewareCore/middlewareCore.compose.js +53 -14
- package/dist/middlewareErrors/index.d.ts +1 -1
- package/dist/middlewareErrors/index.js +1 -1
- package/dist/middlewareErrors/middlewareError.base.d.ts +30 -0
- package/dist/middlewareErrors/middlewareError.base.js +43 -0
- package/dist/middlewarePipeline/middlewarePipeline.core.d.ts +3 -1
- package/dist/middlewarePipeline/middlewarePipeline.core.js +99 -21
- package/dist/middlewareTypes/index.d.ts +1 -1
- package/dist/middlewareTypes/middlewareContext.type.d.ts +81 -12
- package/dist/middlewareUtils/index.d.ts +1 -1
- package/dist/middlewareUtils/index.js +1 -1
- package/dist/middlewareUtils/middlewareUtils.builtins.d.ts +87 -8
- package/dist/middlewareUtils/middlewareUtils.builtins.js +179 -28
- package/package.json +14 -7
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/middlewareCore/index.d.ts.map +0 -1
- package/dist/middlewareCore/index.js.map +0 -1
- package/dist/middlewareCore/middlewareCore.compose.d.ts.map +0 -1
- package/dist/middlewareCore/middlewareCore.compose.js.map +0 -1
- package/dist/middlewareErrors/index.d.ts.map +0 -1
- package/dist/middlewareErrors/index.js.map +0 -1
- package/dist/middlewareErrors/middlewareError.base.d.ts.map +0 -1
- package/dist/middlewareErrors/middlewareError.base.js.map +0 -1
- package/dist/middlewarePipeline/index.d.ts.map +0 -1
- package/dist/middlewarePipeline/index.js.map +0 -1
- package/dist/middlewarePipeline/middlewarePipeline.core.d.ts.map +0 -1
- package/dist/middlewarePipeline/middlewarePipeline.core.js.map +0 -1
- package/dist/middlewareTypes/index.d.ts.map +0 -1
- package/dist/middlewareTypes/index.js.map +0 -1
- package/dist/middlewareTypes/middlewareContext.type.d.ts.map +0 -1
- package/dist/middlewareTypes/middlewareContext.type.js.map +0 -1
- package/dist/middlewareTypes/middlewareDefinition.type.d.ts.map +0 -1
- package/dist/middlewareTypes/middlewareDefinition.type.js.map +0 -1
- package/dist/middlewareUtils/index.d.ts.map +0 -1
- package/dist/middlewareUtils/index.js.map +0 -1
- package/dist/middlewareUtils/middlewareUtils.builtins.d.ts.map +0 -1
- package/dist/middlewareUtils/middlewareUtils.builtins.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zudojs/middleware
|
|
2
2
|
|
|
3
|
-
Composable middleware pipeline with composition,
|
|
3
|
+
Composable middleware pipeline with composition, priority ordering, execution tracking, error handling, and built-in middleware.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -12,28 +12,175 @@ npm install @zudojs/middleware
|
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
createPipeline,
|
|
16
|
+
timeoutMiddleware,
|
|
17
|
+
loggingMiddleware,
|
|
18
|
+
rateLimitMiddleware,
|
|
17
19
|
} from "@zudojs/middleware";
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
interface RequestContext {
|
|
22
|
+
readonly method?: string;
|
|
23
|
+
readonly path?: string;
|
|
24
|
+
readonly key?: string;
|
|
25
|
+
}
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// The handler runs after every middleware has called next().
|
|
28
|
+
const pipeline = createPipeline<RequestContext, string>(
|
|
29
|
+
[
|
|
30
|
+
timeoutMiddleware(5_000),
|
|
31
|
+
loggingMiddleware(),
|
|
32
|
+
rateLimitMiddleware(100, 60_000),
|
|
33
|
+
],
|
|
34
|
+
async (context) => `handled ${context.method} ${context.path}`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const outcome = await pipeline({
|
|
38
|
+
method: "GET",
|
|
39
|
+
path: "/users",
|
|
40
|
+
key: "client-1",
|
|
27
41
|
});
|
|
42
|
+
|
|
43
|
+
if (outcome.success) {
|
|
44
|
+
console.log(outcome.result); // "handled GET /users"
|
|
45
|
+
} else {
|
|
46
|
+
console.error(outcome.error);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`PipelineResult` is a discriminated union: narrowing on `success` gives you a
|
|
51
|
+
non-optional `result`, so no `!` is needed on the happy path.
|
|
52
|
+
|
|
53
|
+
## Composition without the result wrapper
|
|
54
|
+
|
|
55
|
+
`compose` chains middleware around a handler and returns the handler's value
|
|
56
|
+
directly. Use it when you want the raw function rather than a
|
|
57
|
+
`PipelineResult`.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { compose } from "@zudojs/middleware";
|
|
61
|
+
import type { Middleware } from "@zudojs/middleware";
|
|
62
|
+
|
|
63
|
+
const auth: Middleware<RequestContext, string> = async (context, next) => {
|
|
64
|
+
if (context.key === undefined) throw new Error("unauthenticated");
|
|
65
|
+
return next();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handle = compose([auth], async (context) => `hello ${context.key}`);
|
|
69
|
+
await handle({ key: "client-1" });
|
|
28
70
|
```
|
|
29
71
|
|
|
30
|
-
##
|
|
72
|
+
## Priority and toggling
|
|
73
|
+
|
|
74
|
+
`NamedMiddleware` carries a name, a numeric `priority` (lower runs earlier,
|
|
75
|
+
default 100) and an `enabled` flag. `createPipeline` filters and orders them
|
|
76
|
+
for you, and reports which ones ran.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const pipeline = createPipeline(
|
|
80
|
+
[
|
|
81
|
+
{ name: "auth", handler: auth, priority: 10 },
|
|
82
|
+
{ name: "audit", handler: audit, priority: 90, enabled: false },
|
|
83
|
+
],
|
|
84
|
+
handler,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const outcome = await pipeline(context);
|
|
88
|
+
outcome.executedMiddleware; // ["auth"] — "audit" is disabled
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Ordering is stable, so middleware sharing a priority keeps its declared order.
|
|
92
|
+
|
|
93
|
+
## Error handling
|
|
94
|
+
|
|
95
|
+
`errorMode` decides what a failure does:
|
|
96
|
+
|
|
97
|
+
| Mode | Behaviour |
|
|
98
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `"capture"` (default) | Stop and return `{ success: false, error }`. |
|
|
100
|
+
| `"throw"` | Stop and let the error propagate to the caller. |
|
|
101
|
+
| `"continue"` | Record the failure, skip past the failing middleware, keep going. A handler failure is never skipped. |
|
|
102
|
+
|
|
103
|
+
Every failure is also collected in `outcome.errors`, each tagged with the name
|
|
104
|
+
of the middleware that threw (or `"handler"`).
|
|
105
|
+
|
|
106
|
+
> **`"continue"` is not for security middleware.** Stepping past a middleware
|
|
107
|
+
> that threw means an authentication or authorization step that failed is
|
|
108
|
+
> stepped past too, and the handler still runs. A run can come back
|
|
109
|
+
> `success: true` with entries in `outcome.errors`, so under this mode check
|
|
110
|
+
> `outcome.errors` as well as `outcome.success`. Use `"capture"` (the default)
|
|
111
|
+
> or `"throw"` for any chain where a failing step must stop the request.
|
|
112
|
+
|
|
113
|
+
A middleware that catches a downstream failure and throws its own error in its
|
|
114
|
+
place ends the run: there is no downstream result left to keep, so the
|
|
115
|
+
pipeline reports the failure rather than a result that never existed.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const pipeline = createPipeline(list, handler, { errorMode: "continue" });
|
|
119
|
+
const outcome = await pipeline(context);
|
|
120
|
+
outcome.errors; // [{ name: "audit", error: … }]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Cancellation
|
|
124
|
+
|
|
125
|
+
Pass an `AbortSignal` to stop a pipeline between steps. The signal is checked
|
|
126
|
+
before each middleware and before the handler; an aborted run fails with a
|
|
127
|
+
`MiddlewareAbortedError`.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const pipeline = createPipeline(list, handler, { signal: controller.signal });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Built-in middleware
|
|
135
|
+
|
|
136
|
+
- **`loggingMiddleware(logger?, options?)`** — logs start and completion.
|
|
137
|
+
Every interpolated field is escaped, so a path containing newlines cannot
|
|
138
|
+
forge log lines. Error messages are omitted unless you opt in with
|
|
139
|
+
`includeErrorMessage`.
|
|
140
|
+
- **`errorMiddleware(onError?, onReporterError?)`** — reports errors and
|
|
141
|
+
rethrows them. A reporter that throws cannot replace the original error.
|
|
142
|
+
- **`timeoutMiddleware(timeoutMs, options?)`** — fails with
|
|
143
|
+
`MiddlewareTimeoutError`. The timer is always cleared, so a fast request
|
|
144
|
+
leaves nothing pending on the event loop. `options.name` names the
|
|
145
|
+
middleware itself, so two timeouts in one pipeline are distinguishable in
|
|
146
|
+
`executedMiddleware` and in `outcome.errors`.
|
|
147
|
+
- **`rateLimitMiddleware(maxRequests, windowMs, options?)`** — a true sliding
|
|
148
|
+
window keyed on `context.key`, with key eviction and a configurable cap.
|
|
149
|
+
Rejections throw `MiddlewareRateLimitError`, which carries `retryAfterMs`
|
|
150
|
+
for a `Retry-After` header.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { MiddlewareRateLimitError } from "@zudojs/middleware";
|
|
154
|
+
|
|
155
|
+
if (outcome.error instanceof MiddlewareRateLimitError) {
|
|
156
|
+
respond(429, { retryAfterMs: outcome.error.retryAfterMs });
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Rate-limit state is per-instance and in-process: behind more than one replica,
|
|
161
|
+
each process enforces its own limit.
|
|
162
|
+
|
|
163
|
+
## Timing
|
|
164
|
+
|
|
165
|
+
`withTiming` wraps any middleware and reports slow executions. It is
|
|
166
|
+
transparent — the return value and any error pass through unchanged.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const timed = withTiming("db-lookup", lookup, {
|
|
170
|
+
thresholdMs: 50,
|
|
171
|
+
logger: (message) => logger.warn(message),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Errors
|
|
176
|
+
|
|
177
|
+
All errors extend `MiddlewareError` (itself a `BaseError` from
|
|
178
|
+
`@zudojs/errors`), so a single `instanceof` catches everything from this
|
|
179
|
+
package:
|
|
31
180
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- Context propagation
|
|
36
|
-
- Early termination support
|
|
181
|
+
`MiddlewareTimeoutError` · `MiddlewareNextCalledMultipleTimesError` ·
|
|
182
|
+
`MiddlewareLimitExceededError` · `MiddlewareDepthExceededError` ·
|
|
183
|
+
`MiddlewareRateLimitError` · `MiddlewareAbortedError`
|
|
37
184
|
|
|
38
185
|
## Use Cases
|
|
39
186
|
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module middlewareCore
|
|
5
5
|
*/
|
|
6
|
-
export { compose, resolveMiddleware, withTiming, } from "./middlewareCore.compose.js";
|
|
6
|
+
export { compose, resolveMiddleware, resolveNamedMiddleware, withTiming, MAX_DEPTH, type ComposeOptions, type TimingOptions, } from "./middlewareCore.compose.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module middlewareCore
|
|
5
5
|
*/
|
|
6
|
-
export { compose, resolveMiddleware, withTiming, } from "./middlewareCore.compose.js";
|
|
6
|
+
export { compose, resolveMiddleware, resolveNamedMiddleware, withTiming, MAX_DEPTH, } from "./middlewareCore.compose.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* Each middleware calls `next()` to proceed to the next one.
|
|
8
8
|
*/
|
|
9
9
|
import type { Middleware, NamedMiddleware } from "../middlewareTypes/middlewareDefinition.type.js";
|
|
10
|
+
/** Default ceiling on how deeply a composed chain may nest. */
|
|
11
|
+
export declare const MAX_DEPTH = 100;
|
|
12
|
+
/** Options for {@link compose}. */
|
|
13
|
+
export interface ComposeOptions {
|
|
14
|
+
/** Maximum chain depth. Default: {@link MAX_DEPTH}. */
|
|
15
|
+
readonly maxDepth?: number;
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
18
|
* Compose an array of middleware into a single function.
|
|
12
19
|
*
|
|
@@ -15,18 +22,43 @@ import type { Middleware, NamedMiddleware } from "../middlewareTypes/middlewareD
|
|
|
15
22
|
*
|
|
16
23
|
* @param middlewareList - Array of middleware functions
|
|
17
24
|
* @param handler - The final handler to execute after all middleware
|
|
25
|
+
* @param options - Composition limits
|
|
18
26
|
* @returns Composed function
|
|
27
|
+
* @throws MiddlewareDepthExceededError if the chain is longer than `maxDepth`
|
|
19
28
|
*/
|
|
20
|
-
export declare function compose<TContext, TResult>(middlewareList: readonly Middleware<TContext, TResult>[], handler: (context: TContext) => Promise<TResult
|
|
29
|
+
export declare function compose<TContext, TResult>(middlewareList: readonly Middleware<TContext, TResult>[], handler: (context: TContext) => Promise<TResult>, options?: ComposeOptions): (context: TContext) => Promise<TResult>;
|
|
21
30
|
/**
|
|
22
|
-
*
|
|
31
|
+
* Filter disabled middleware and sort the rest by priority.
|
|
32
|
+
*
|
|
33
|
+
* Ordering is stable, so middleware sharing a priority keeps its input order.
|
|
34
|
+
*
|
|
35
|
+
* @param middlewareList - Array of named middleware
|
|
36
|
+
* @returns Enabled middleware, in execution order, with names intact
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveNamedMiddleware<TContext, TResult>(middlewareList: readonly NamedMiddleware<TContext, TResult>[]): NamedMiddleware<TContext, TResult>[];
|
|
39
|
+
/**
|
|
40
|
+
* Sort and filter named middleware by priority, returning bare handlers.
|
|
41
|
+
*
|
|
42
|
+
* Prefer {@link resolveNamedMiddleware} when the names are needed — this is a
|
|
43
|
+
* thin projection of it.
|
|
23
44
|
*
|
|
24
45
|
* @param middlewareList - Array of named middleware
|
|
25
46
|
* @returns Sorted and filtered array of middleware handler functions
|
|
26
47
|
*/
|
|
27
48
|
export declare function resolveMiddleware<TContext, TResult>(middlewareList: readonly NamedMiddleware<TContext, TResult>[]): Middleware<TContext, TResult>[];
|
|
49
|
+
/** Options for {@link withTiming}. */
|
|
50
|
+
export interface TimingOptions {
|
|
51
|
+
/** Log only when the middleware takes at least this long. Default: 100ms. */
|
|
52
|
+
readonly thresholdMs?: number;
|
|
53
|
+
/** Where to report slow middleware. Default: `console.warn`. */
|
|
54
|
+
readonly logger?: (message: string) => void;
|
|
55
|
+
}
|
|
28
56
|
/**
|
|
29
|
-
*
|
|
57
|
+
* Wrap a middleware so that slow executions are reported.
|
|
58
|
+
*
|
|
59
|
+
* The wrapper is transparent: the inner middleware's return value and any
|
|
60
|
+
* error it throws pass through unchanged, and the timing is reported either
|
|
61
|
+
* way.
|
|
30
62
|
*/
|
|
31
|
-
export declare function withTiming<TContext>(name: string, middleware: Middleware<TContext,
|
|
63
|
+
export declare function withTiming<TContext, TResult = void>(name: string, middleware: Middleware<TContext, TResult>, options?: TimingOptions): NamedMiddleware<TContext, TResult>;
|
|
32
64
|
//# sourceMappingURL=middlewareCore.compose.d.ts.map
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Middleware executes in order (first added = first executed).
|
|
7
7
|
* Each middleware calls `next()` to proceed to the next one.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import { MiddlewareDepthExceededError, MiddlewareNextCalledMultipleTimesError, } from "../middlewareErrors/middlewareError.base.js";
|
|
10
|
+
/** Default ceiling on how deeply a composed chain may nest. */
|
|
11
|
+
export const MAX_DEPTH = 100;
|
|
10
12
|
/**
|
|
11
13
|
* Compose an array of middleware into a single function.
|
|
12
14
|
*
|
|
@@ -15,9 +17,15 @@ const MAX_DEPTH = 100;
|
|
|
15
17
|
*
|
|
16
18
|
* @param middlewareList - Array of middleware functions
|
|
17
19
|
* @param handler - The final handler to execute after all middleware
|
|
20
|
+
* @param options - Composition limits
|
|
18
21
|
* @returns Composed function
|
|
22
|
+
* @throws MiddlewareDepthExceededError if the chain is longer than `maxDepth`
|
|
19
23
|
*/
|
|
20
|
-
export function compose(middlewareList, handler) {
|
|
24
|
+
export function compose(middlewareList, handler, options) {
|
|
25
|
+
const maxDepth = options?.maxDepth ?? MAX_DEPTH;
|
|
26
|
+
if (middlewareList.length > maxDepth) {
|
|
27
|
+
throw new MiddlewareDepthExceededError(maxDepth);
|
|
28
|
+
}
|
|
21
29
|
if (middlewareList.length === 0) {
|
|
22
30
|
return handler;
|
|
23
31
|
}
|
|
@@ -25,7 +33,12 @@ export function compose(middlewareList, handler) {
|
|
|
25
33
|
let index = -1;
|
|
26
34
|
async function dispatch(i) {
|
|
27
35
|
if (i <= index) {
|
|
28
|
-
|
|
36
|
+
// dispatch(i) is invoked by the middleware at i - 1, so that is the
|
|
37
|
+
// one that called next() again.
|
|
38
|
+
throw new MiddlewareNextCalledMultipleTimesError(`middleware[${i - 1}]`);
|
|
39
|
+
}
|
|
40
|
+
if (i > maxDepth) {
|
|
41
|
+
throw new MiddlewareDepthExceededError(maxDepth);
|
|
29
42
|
}
|
|
30
43
|
index = i;
|
|
31
44
|
if (i < middlewareList.length) {
|
|
@@ -38,29 +51,55 @@ export function compose(middlewareList, handler) {
|
|
|
38
51
|
};
|
|
39
52
|
}
|
|
40
53
|
/**
|
|
41
|
-
*
|
|
54
|
+
* Filter disabled middleware and sort the rest by priority.
|
|
55
|
+
*
|
|
56
|
+
* Ordering is stable, so middleware sharing a priority keeps its input order.
|
|
42
57
|
*
|
|
43
58
|
* @param middlewareList - Array of named middleware
|
|
44
|
-
* @returns
|
|
59
|
+
* @returns Enabled middleware, in execution order, with names intact
|
|
45
60
|
*/
|
|
46
|
-
export function
|
|
61
|
+
export function resolveNamedMiddleware(middlewareList) {
|
|
47
62
|
return middlewareList
|
|
48
63
|
.filter((mw) => mw.enabled !== false)
|
|
49
|
-
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100))
|
|
50
|
-
.map((mw) => mw.handler);
|
|
64
|
+
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
51
65
|
}
|
|
52
66
|
/**
|
|
53
|
-
*
|
|
67
|
+
* Sort and filter named middleware by priority, returning bare handlers.
|
|
68
|
+
*
|
|
69
|
+
* Prefer {@link resolveNamedMiddleware} when the names are needed — this is a
|
|
70
|
+
* thin projection of it.
|
|
71
|
+
*
|
|
72
|
+
* @param middlewareList - Array of named middleware
|
|
73
|
+
* @returns Sorted and filtered array of middleware handler functions
|
|
54
74
|
*/
|
|
55
|
-
export function
|
|
75
|
+
export function resolveMiddleware(middlewareList) {
|
|
76
|
+
return resolveNamedMiddleware(middlewareList).map((mw) => mw.handler);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Wrap a middleware so that slow executions are reported.
|
|
80
|
+
*
|
|
81
|
+
* The wrapper is transparent: the inner middleware's return value and any
|
|
82
|
+
* error it throws pass through unchanged, and the timing is reported either
|
|
83
|
+
* way.
|
|
84
|
+
*/
|
|
85
|
+
export function withTiming(name, middleware, options) {
|
|
86
|
+
const thresholdMs = options?.thresholdMs ?? 100;
|
|
87
|
+
const log = options?.logger ??
|
|
88
|
+
((message) => {
|
|
89
|
+
console.warn(message);
|
|
90
|
+
});
|
|
56
91
|
return {
|
|
57
92
|
name,
|
|
58
93
|
handler: async (ctx, next) => {
|
|
59
94
|
const start = performance.now();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
try {
|
|
96
|
+
return await middleware(ctx, next);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
const duration = performance.now() - start;
|
|
100
|
+
if (duration >= thresholdMs) {
|
|
101
|
+
log(`[middleware] ${name} took ${duration.toFixed(1)}ms`);
|
|
102
|
+
}
|
|
64
103
|
}
|
|
65
104
|
},
|
|
66
105
|
};
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module middlewareErrors
|
|
5
5
|
*/
|
|
6
|
-
export { MiddlewareError, MiddlewareTimeoutError, MiddlewareNextCalledMultipleTimesError, } from "./middlewareError.base.js";
|
|
6
|
+
export { MiddlewareError, MiddlewareTimeoutError, MiddlewareNextCalledMultipleTimesError, MiddlewareLimitExceededError, MiddlewareDepthExceededError, MiddlewareRateLimitError, MiddlewareAbortedError, } from "./middlewareError.base.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module middlewareErrors
|
|
5
5
|
*/
|
|
6
|
-
export { MiddlewareError, MiddlewareTimeoutError, MiddlewareNextCalledMultipleTimesError, } from "./middlewareError.base.js";
|
|
6
|
+
export { MiddlewareError, MiddlewareTimeoutError, MiddlewareNextCalledMultipleTimesError, MiddlewareLimitExceededError, MiddlewareDepthExceededError, MiddlewareRateLimitError, MiddlewareAbortedError, } from "./middlewareError.base.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -25,4 +25,34 @@ export declare class MiddlewareTimeoutError extends MiddlewareError {
|
|
|
25
25
|
export declare class MiddlewareNextCalledMultipleTimesError extends MiddlewareError {
|
|
26
26
|
constructor(middlewareName: string);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Error thrown when a pipeline is configured with more middleware than allowed.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MiddlewareLimitExceededError extends MiddlewareError {
|
|
32
|
+
constructor(count: number, maximum: number);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when the middleware chain nests deeper than the allowed limit.
|
|
36
|
+
*/
|
|
37
|
+
export declare class MiddlewareDepthExceededError extends MiddlewareError {
|
|
38
|
+
constructor(maxDepth: number);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Error thrown when a rate limit is exceeded.
|
|
42
|
+
*
|
|
43
|
+
* `retryAfterMs` tells the caller how long to wait before retrying, which is
|
|
44
|
+
* what an HTTP adapter needs to emit a `Retry-After` header alongside a 429.
|
|
45
|
+
*/
|
|
46
|
+
export declare class MiddlewareRateLimitError extends MiddlewareError {
|
|
47
|
+
readonly retryAfterMs: number;
|
|
48
|
+
readonly limit: number;
|
|
49
|
+
readonly windowMs: number;
|
|
50
|
+
constructor(limit: number, windowMs: number, retryAfterMs: number);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Error thrown when a pipeline is aborted through its `AbortSignal`.
|
|
54
|
+
*/
|
|
55
|
+
export declare class MiddlewareAbortedError extends MiddlewareError {
|
|
56
|
+
constructor(reason?: unknown);
|
|
57
|
+
}
|
|
28
58
|
//# sourceMappingURL=middlewareError.base.d.ts.map
|
|
@@ -40,4 +40,47 @@ export class MiddlewareNextCalledMultipleTimesError extends MiddlewareError {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Error thrown when a pipeline is configured with more middleware than allowed.
|
|
45
|
+
*/
|
|
46
|
+
export class MiddlewareLimitExceededError extends MiddlewareError {
|
|
47
|
+
constructor(count, maximum) {
|
|
48
|
+
super(`Pipeline has ${count} middleware, exceeding the maximum of ${maximum}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Error thrown when the middleware chain nests deeper than the allowed limit.
|
|
53
|
+
*/
|
|
54
|
+
export class MiddlewareDepthExceededError extends MiddlewareError {
|
|
55
|
+
constructor(maxDepth) {
|
|
56
|
+
super(`Middleware chain exceeded the maximum depth of ${maxDepth}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Error thrown when a rate limit is exceeded.
|
|
61
|
+
*
|
|
62
|
+
* `retryAfterMs` tells the caller how long to wait before retrying, which is
|
|
63
|
+
* what an HTTP adapter needs to emit a `Retry-After` header alongside a 429.
|
|
64
|
+
*/
|
|
65
|
+
export class MiddlewareRateLimitError extends MiddlewareError {
|
|
66
|
+
retryAfterMs;
|
|
67
|
+
limit;
|
|
68
|
+
windowMs;
|
|
69
|
+
constructor(limit, windowMs, retryAfterMs) {
|
|
70
|
+
super(`Rate limit exceeded: ${limit} requests per ${windowMs}ms`, {
|
|
71
|
+
middlewareName: "rate-limit",
|
|
72
|
+
});
|
|
73
|
+
this.retryAfterMs = retryAfterMs;
|
|
74
|
+
this.limit = limit;
|
|
75
|
+
this.windowMs = windowMs;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Error thrown when a pipeline is aborted through its `AbortSignal`.
|
|
80
|
+
*/
|
|
81
|
+
export class MiddlewareAbortedError extends MiddlewareError {
|
|
82
|
+
constructor(reason) {
|
|
83
|
+
super("Middleware pipeline aborted", { cause: reason });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
43
86
|
//# sourceMappingURL=middlewareError.base.js.map
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module middlewarePipeline/middlewarePipeline
|
|
5
5
|
*/
|
|
6
|
-
import type { NamedMiddleware } from "../middlewareTypes/middlewareDefinition.type.js";
|
|
7
6
|
import type { PipelineResult, PipelineOptions } from "../middlewareTypes/middlewareContext.type.js";
|
|
7
|
+
import type { NamedMiddleware } from "../middlewareTypes/middlewareDefinition.type.js";
|
|
8
8
|
/**
|
|
9
9
|
* Create a middleware pipeline that tracks execution.
|
|
10
10
|
*
|
|
@@ -12,6 +12,8 @@ import type { PipelineResult, PipelineOptions } from "../middlewareTypes/middlew
|
|
|
12
12
|
* @param handler - Final handler function
|
|
13
13
|
* @param options - Pipeline configuration
|
|
14
14
|
* @returns Pipeline execution function
|
|
15
|
+
* @throws MiddlewareLimitExceededError if more middleware are enabled than
|
|
16
|
+
* `maxMiddleware` allows
|
|
15
17
|
*/
|
|
16
18
|
export declare function createPipeline<TContext, TResult>(middlewareList: readonly NamedMiddleware<TContext, TResult>[], handler: (context: TContext) => Promise<TResult>, options?: PipelineOptions): (context: TContext) => Promise<PipelineResult<TResult>>;
|
|
17
19
|
//# sourceMappingURL=middlewarePipeline.core.d.ts.map
|
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module middlewarePipeline/middlewarePipeline
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { resolveNamedMiddleware } from "../middlewareCore/middlewareCore.compose.js";
|
|
7
|
+
import { MiddlewareAbortedError, MiddlewareLimitExceededError, MiddlewareNextCalledMultipleTimesError, } from "../middlewareErrors/middlewareError.base.js";
|
|
7
8
|
const DEFAULT_MAX = 50;
|
|
9
|
+
/** Name recorded for a failure thrown by the final handler. */
|
|
10
|
+
const HANDLER_NAME = "handler";
|
|
11
|
+
function resolveErrorMode(options) {
|
|
12
|
+
if (options?.errorMode)
|
|
13
|
+
return options.errorMode;
|
|
14
|
+
if (options?.stopOnError === false)
|
|
15
|
+
return "throw";
|
|
16
|
+
return "capture";
|
|
17
|
+
}
|
|
8
18
|
/**
|
|
9
19
|
* Create a middleware pipeline that tracks execution.
|
|
10
20
|
*
|
|
@@ -12,33 +22,93 @@ const DEFAULT_MAX = 50;
|
|
|
12
22
|
* @param handler - Final handler function
|
|
13
23
|
* @param options - Pipeline configuration
|
|
14
24
|
* @returns Pipeline execution function
|
|
25
|
+
* @throws MiddlewareLimitExceededError if more middleware are enabled than
|
|
26
|
+
* `maxMiddleware` allows
|
|
15
27
|
*/
|
|
16
28
|
export function createPipeline(middlewareList, handler, options) {
|
|
17
|
-
const resolved =
|
|
29
|
+
const resolved = resolveNamedMiddleware(middlewareList);
|
|
18
30
|
const maxMiddleware = options?.maxMiddleware ?? DEFAULT_MAX;
|
|
19
|
-
const
|
|
31
|
+
const errorMode = resolveErrorMode(options);
|
|
32
|
+
const signal = options?.signal;
|
|
20
33
|
if (resolved.length > maxMiddleware) {
|
|
21
|
-
throw new
|
|
34
|
+
throw new MiddlewareLimitExceededError(resolved.length, maxMiddleware);
|
|
22
35
|
}
|
|
23
|
-
const enabledNames = middlewareList
|
|
24
|
-
.filter((mw) => mw.enabled !== false)
|
|
25
|
-
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100))
|
|
26
|
-
.map((mw) => mw.name);
|
|
27
36
|
return async (context) => {
|
|
28
37
|
const startTime = performance.now();
|
|
29
38
|
const executed = [];
|
|
39
|
+
const errors = [];
|
|
30
40
|
let index = -1;
|
|
41
|
+
/**
|
|
42
|
+
* The error the final handler threw, if it did. Middleware still sees the
|
|
43
|
+
* error itself — wrapping it would break `catch (e) { if (e instanceof
|
|
44
|
+
* HttpError) … }` in user middleware — so identity is what marks it as
|
|
45
|
+
* the handler's, and that is what stops `errorMode: "continue"` from
|
|
46
|
+
* treating it as a middleware failure it can step past.
|
|
47
|
+
*/
|
|
48
|
+
let handlerError;
|
|
49
|
+
function throwIfAborted() {
|
|
50
|
+
if (signal?.aborted) {
|
|
51
|
+
throw new MiddlewareAbortedError(signal.reason);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Errors that `"continue"` must never swallow. */
|
|
55
|
+
function isFatal(error) {
|
|
56
|
+
return ((handlerError !== undefined && error === handlerError.error) ||
|
|
57
|
+
error instanceof MiddlewareAbortedError ||
|
|
58
|
+
error instanceof MiddlewareNextCalledMultipleTimesError);
|
|
59
|
+
}
|
|
31
60
|
async function dispatch(i) {
|
|
32
61
|
if (i <= index) {
|
|
33
|
-
|
|
62
|
+
// dispatch(i) is invoked by the middleware at i - 1, so that is the
|
|
63
|
+
// one that called next() again.
|
|
64
|
+
const name = resolved[i - 1]?.name ?? `middleware[${i - 1}]`;
|
|
65
|
+
throw new MiddlewareNextCalledMultipleTimesError(name);
|
|
34
66
|
}
|
|
35
67
|
index = i;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
68
|
+
throwIfAborted();
|
|
69
|
+
if (i >= resolved.length) {
|
|
70
|
+
try {
|
|
71
|
+
return await handler(context);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
handlerError = { error };
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const mw = resolved[i];
|
|
79
|
+
executed.push(mw.name);
|
|
80
|
+
if (errorMode !== "continue") {
|
|
81
|
+
return mw.handler(context, () => dispatch(i + 1));
|
|
82
|
+
}
|
|
83
|
+
let advanced = false;
|
|
84
|
+
// Boxed rather than a bare `TResult | undefined`, so "the downstream
|
|
85
|
+
// chain produced a value" is distinguishable from "it produced
|
|
86
|
+
// `undefined`" without an assertion.
|
|
87
|
+
let downstream;
|
|
88
|
+
try {
|
|
89
|
+
return await mw.handler(context, async () => {
|
|
90
|
+
advanced = true;
|
|
91
|
+
const value = await dispatch(i + 1);
|
|
92
|
+
downstream = { value };
|
|
93
|
+
return value;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (isFatal(error))
|
|
98
|
+
throw error;
|
|
99
|
+
if (advanced && downstream === undefined) {
|
|
100
|
+
// next() was entered but never produced a result — the middleware
|
|
101
|
+
// caught the downstream failure and threw something of its own.
|
|
102
|
+
// There is no downstream result to keep and no step left to
|
|
103
|
+
// continue to, so returning `undefined` as if it were a `TResult`
|
|
104
|
+
// would report a successful run that never happened.
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
errors.push({ name: mw.name, error });
|
|
108
|
+
// The chain already ran past this middleware, so its downstream
|
|
109
|
+
// result stands; otherwise pick up at the next middleware.
|
|
110
|
+
return downstream ? downstream.value : dispatch(i + 1);
|
|
40
111
|
}
|
|
41
|
-
return handler(context);
|
|
42
112
|
}
|
|
43
113
|
try {
|
|
44
114
|
const result = await dispatch(0);
|
|
@@ -47,18 +117,26 @@ export function createPipeline(middlewareList, handler, options) {
|
|
|
47
117
|
result,
|
|
48
118
|
durationMs: performance.now() - startTime,
|
|
49
119
|
executedMiddleware: executed,
|
|
120
|
+
errors,
|
|
50
121
|
};
|
|
51
122
|
}
|
|
52
123
|
catch (error) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
124
|
+
const fromHandler = handlerError !== undefined && error === handlerError.error;
|
|
125
|
+
if (!errors.some((failure) => failure.error === error)) {
|
|
126
|
+
errors.push({
|
|
127
|
+
name: fromHandler ? HANDLER_NAME : (executed.at(-1) ?? HANDLER_NAME),
|
|
56
128
|
error,
|
|
57
|
-
|
|
58
|
-
executedMiddleware: executed,
|
|
59
|
-
};
|
|
129
|
+
});
|
|
60
130
|
}
|
|
61
|
-
throw
|
|
131
|
+
if (errorMode === "throw")
|
|
132
|
+
throw error;
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error,
|
|
136
|
+
durationMs: performance.now() - startTime,
|
|
137
|
+
executedMiddleware: executed,
|
|
138
|
+
errors,
|
|
139
|
+
};
|
|
62
140
|
}
|
|
63
141
|
};
|
|
64
142
|
}
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* @module middlewareTypes
|
|
5
5
|
*/
|
|
6
6
|
export { type Middleware, type NamedMiddleware, type MiddlewareFactory, } from "./middlewareDefinition.type.js";
|
|
7
|
-
export { type PipelineResult, type PipelineOptions, } from "./middlewareContext.type.js";
|
|
7
|
+
export { type PipelineResult, type PipelineSuccess, type PipelineFailure, type PipelineMiddlewareFailure, type PipelineErrorMode, type PipelineOptions, } from "./middlewareContext.type.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|