@zudojs/middleware 1.0.1 → 1.0.3
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 +21 -8
- package/dist/middlewareCore/middlewareCore.compose.d.ts +1 -1
- package/dist/middlewareCore/middlewareCore.compose.js +1 -4
- package/dist/middlewareErrors/middlewareError.base.d.ts +5 -52
- package/dist/middlewareErrors/middlewareError.base.js +5 -80
- package/dist/middlewareUtils/middlewareUtils.builtins.d.ts +4 -0
- package/dist/middlewareUtils/middlewareUtils.builtins.js +9 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Composable middleware pipeline with composition, priority ordering, execution tracking, error handling, and built-in middleware.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-middleware](https://zudojs.oyinlola.site/docs/packages-middleware) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-middleware.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -28,7 +34,7 @@ interface RequestContext {
|
|
|
28
34
|
const pipeline = createPipeline<RequestContext, string>(
|
|
29
35
|
[
|
|
30
36
|
timeoutMiddleware(5_000),
|
|
31
|
-
loggingMiddleware(),
|
|
37
|
+
loggingMiddleware((line) => console.info(line)),
|
|
32
38
|
rateLimitMiddleware(100, 60_000),
|
|
33
39
|
],
|
|
34
40
|
async (context) => `handled ${context.method} ${context.path}`,
|
|
@@ -133,8 +139,10 @@ const pipeline = createPipeline(list, handler, { signal: controller.signal });
|
|
|
133
139
|
|
|
134
140
|
## Built-in middleware
|
|
135
141
|
|
|
136
|
-
- **`loggingMiddleware(logger?, options?)`** — logs start and completion
|
|
137
|
-
|
|
142
|
+
- **`loggingMiddleware(logger?, options?)`** — logs start and completion
|
|
143
|
+
to `logger`, a `(line: string) => void` sink such as
|
|
144
|
+
`(line) => log.info(line)` from `@zudojs/logger`. Without a sink it writes
|
|
145
|
+
nothing (it never falls back to the console). Every interpolated field is escaped, so a path containing newlines cannot
|
|
138
146
|
forge log lines. Error messages are omitted unless you opt in with
|
|
139
147
|
`includeErrorMessage`.
|
|
140
148
|
- **`errorMiddleware(onError?, onReporterError?)`** — reports errors and
|
|
@@ -147,7 +155,9 @@ const pipeline = createPipeline(list, handler, { signal: controller.signal });
|
|
|
147
155
|
- **`rateLimitMiddleware(maxRequests, windowMs, options?)`** — a true sliding
|
|
148
156
|
window keyed on `context.key`, with key eviction and a configurable cap.
|
|
149
157
|
Rejections throw `MiddlewareRateLimitError`, which carries `retryAfterMs`
|
|
150
|
-
for a `Retry-After` header.
|
|
158
|
+
for a `Retry-After` header. A rejected request also counts as recent
|
|
159
|
+
activity, so a throttled key is never the first one evicted when the key
|
|
160
|
+
cap is reached.
|
|
151
161
|
|
|
152
162
|
```typescript
|
|
153
163
|
import { MiddlewareRateLimitError } from "@zudojs/middleware";
|
|
@@ -162,7 +172,8 @@ each process enforces its own limit.
|
|
|
162
172
|
|
|
163
173
|
## Timing
|
|
164
174
|
|
|
165
|
-
`withTiming` wraps any middleware and reports slow executions
|
|
175
|
+
`withTiming` wraps any middleware and reports slow executions to
|
|
176
|
+
`options.logger`. Without a logger nothing is reported. It is
|
|
166
177
|
transparent — the return value and any error pass through unchanged.
|
|
167
178
|
|
|
168
179
|
```typescript
|
|
@@ -174,9 +185,11 @@ const timed = withTiming("db-lookup", lookup, {
|
|
|
174
185
|
|
|
175
186
|
## Errors
|
|
176
187
|
|
|
177
|
-
All errors extend `MiddlewareError
|
|
178
|
-
`@zudojs/errors`
|
|
179
|
-
|
|
188
|
+
All errors extend `MiddlewareError`, which is the class owned by
|
|
189
|
+
`@zudojs/errors` (re-exported here). `MiddlewareTimeoutError` and
|
|
190
|
+
`MiddlewareNextCalledMultipleTimesError` are the `@zudojs/errors` classes
|
|
191
|
+
too, so an `instanceof` check against either import path matches, and a
|
|
192
|
+
single `instanceof MiddlewareError` catches everything from this package:
|
|
180
193
|
|
|
181
194
|
`MiddlewareTimeoutError` · `MiddlewareNextCalledMultipleTimesError` ·
|
|
182
195
|
`MiddlewareLimitExceededError` · `MiddlewareDepthExceededError` ·
|
|
@@ -50,7 +50,7 @@ export declare function resolveMiddleware<TContext, TResult>(middlewareList: rea
|
|
|
50
50
|
export interface TimingOptions {
|
|
51
51
|
/** Log only when the middleware takes at least this long. Default: 100ms. */
|
|
52
52
|
readonly thresholdMs?: number;
|
|
53
|
-
/** Where to report slow middleware. Default:
|
|
53
|
+
/** Where to report slow middleware. Default: nothing is reported. */
|
|
54
54
|
readonly logger?: (message: string) => void;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
@@ -84,10 +84,7 @@ export function resolveMiddleware(middlewareList) {
|
|
|
84
84
|
*/
|
|
85
85
|
export function withTiming(name, middleware, options) {
|
|
86
86
|
const thresholdMs = options?.thresholdMs ?? 100;
|
|
87
|
-
const log = options?.logger ??
|
|
88
|
-
((message) => {
|
|
89
|
-
console.warn(message);
|
|
90
|
-
});
|
|
87
|
+
const log = options?.logger ?? (() => undefined);
|
|
91
88
|
return {
|
|
92
89
|
name,
|
|
93
90
|
handler: async (ctx, next) => {
|
|
@@ -1,58 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Middleware-specific error classes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Error thrown when a middleware pipeline fails.
|
|
9
|
-
*/
|
|
10
|
-
export declare class MiddlewareError extends BaseError {
|
|
11
|
-
constructor(message: string, options?: {
|
|
12
|
-
readonly middlewareName?: string;
|
|
13
|
-
readonly cause?: unknown;
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Error thrown when a middleware exceeds its timeout.
|
|
18
|
-
*/
|
|
19
|
-
export declare class MiddlewareTimeoutError extends MiddlewareError {
|
|
20
|
-
constructor(middlewareName: string, timeoutMs: number);
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Error thrown when a middleware calls next() multiple times.
|
|
24
|
-
*/
|
|
25
|
-
export declare class MiddlewareNextCalledMultipleTimesError extends MiddlewareError {
|
|
26
|
-
constructor(middlewareName: string);
|
|
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.
|
|
4
|
+
* Every class is owned by `@zudojs/errors` and re-exported here, so
|
|
5
|
+
* `instanceof` checks against either import path match the same errors.
|
|
6
|
+
* Codes, messages and fields are unchanged.
|
|
42
7
|
*
|
|
43
|
-
*
|
|
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`.
|
|
8
|
+
* @module middlewareErrors
|
|
54
9
|
*/
|
|
55
|
-
export
|
|
56
|
-
constructor(reason?: unknown);
|
|
57
|
-
}
|
|
10
|
+
export { MiddlewareError, MiddlewareTimeoutError, MiddlewareNextCalledMultipleTimesError, MiddlewareLimitExceededError, MiddlewareDepthExceededError, MiddlewareRateLimitError, MiddlewareAbortedError, } from "@zudojs/errors";
|
|
58
11
|
//# sourceMappingURL=middlewareError.base.d.ts.map
|
|
@@ -1,86 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Middleware-specific error classes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Error thrown when a middleware pipeline fails.
|
|
9
|
-
*/
|
|
10
|
-
export class MiddlewareError extends BaseError {
|
|
11
|
-
constructor(message, options) {
|
|
12
|
-
super(message, {
|
|
13
|
-
code: ErrorCode.OPERATION_FAILED,
|
|
14
|
-
category: ErrorCategory.INTERNAL,
|
|
15
|
-
severity: ErrorSeverity.ERROR,
|
|
16
|
-
metadata: {
|
|
17
|
-
middlewareName: options?.middlewareName,
|
|
18
|
-
},
|
|
19
|
-
cause: options?.cause,
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Error thrown when a middleware exceeds its timeout.
|
|
25
|
-
*/
|
|
26
|
-
export class MiddlewareTimeoutError extends MiddlewareError {
|
|
27
|
-
constructor(middlewareName, timeoutMs) {
|
|
28
|
-
super(`Middleware "${middlewareName}" timed out after ${timeoutMs}ms`, {
|
|
29
|
-
middlewareName,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Error thrown when a middleware calls next() multiple times.
|
|
35
|
-
*/
|
|
36
|
-
export class MiddlewareNextCalledMultipleTimesError extends MiddlewareError {
|
|
37
|
-
constructor(middlewareName) {
|
|
38
|
-
super(`Middleware "${middlewareName}" called next() multiple times`, {
|
|
39
|
-
middlewareName,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
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.
|
|
4
|
+
* Every class is owned by `@zudojs/errors` and re-exported here, so
|
|
5
|
+
* `instanceof` checks against either import path match the same errors.
|
|
6
|
+
* Codes, messages and fields are unchanged.
|
|
61
7
|
*
|
|
62
|
-
*
|
|
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`.
|
|
8
|
+
* @module middlewareErrors
|
|
80
9
|
*/
|
|
81
|
-
export
|
|
82
|
-
constructor(reason) {
|
|
83
|
-
super("Middleware pipeline aborted", { cause: reason });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
10
|
+
export { MiddlewareError, MiddlewareTimeoutError, MiddlewareNextCalledMultipleTimesError, MiddlewareLimitExceededError, MiddlewareDepthExceededError, MiddlewareRateLimitError, MiddlewareAbortedError, } from "@zudojs/errors";
|
|
86
11
|
//# sourceMappingURL=middlewareError.base.js.map
|
|
@@ -34,6 +34,10 @@ export interface LoggingOptions {
|
|
|
34
34
|
*
|
|
35
35
|
* Logs request start, completion, and errors. Every interpolated field is
|
|
36
36
|
* escaped, so a path containing newlines cannot forge log lines.
|
|
37
|
+
*
|
|
38
|
+
* Lines go to `logger`, typically a `@zudojs/logger` method such as
|
|
39
|
+
* `(line) => log.info(line)`. Without one the middleware writes nothing;
|
|
40
|
+
* it never falls back to the console.
|
|
37
41
|
*/
|
|
38
42
|
export declare function loggingMiddleware<TResult = void>(logger?: (msg: string) => void, options?: LoggingOptions): NamedMiddleware<LoggingContext, TResult>;
|
|
39
43
|
/**
|
|
@@ -38,14 +38,21 @@ export function sanitizeLogValue(value, maxLength = MAX_LOG_FIELD_LENGTH) {
|
|
|
38
38
|
? `${escaped.slice(0, maxLength)}…`
|
|
39
39
|
: escaped;
|
|
40
40
|
}
|
|
41
|
+
function noopLog() {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
41
44
|
/**
|
|
42
45
|
* Create a logging middleware.
|
|
43
46
|
*
|
|
44
47
|
* Logs request start, completion, and errors. Every interpolated field is
|
|
45
48
|
* escaped, so a path containing newlines cannot forge log lines.
|
|
49
|
+
*
|
|
50
|
+
* Lines go to `logger`, typically a `@zudojs/logger` method such as
|
|
51
|
+
* `(line) => log.info(line)`. Without one the middleware writes nothing;
|
|
52
|
+
* it never falls back to the console.
|
|
46
53
|
*/
|
|
47
54
|
export function loggingMiddleware(logger, options) {
|
|
48
|
-
const log = logger ??
|
|
55
|
+
const log = logger ?? noopLog;
|
|
49
56
|
const maxFieldLength = options?.maxFieldLength ?? MAX_LOG_FIELD_LENGTH;
|
|
50
57
|
const includeErrorMessage = options?.includeErrorMessage ?? false;
|
|
51
58
|
return {
|
|
@@ -234,6 +241,7 @@ export function rateLimitMiddleware(maxRequests, windowMs, options) {
|
|
|
234
241
|
sweep(now);
|
|
235
242
|
const live = prune(hits.get(key) ?? [], now);
|
|
236
243
|
if (live.length >= maxRequests) {
|
|
244
|
+
hits.delete(key);
|
|
237
245
|
hits.set(key, live);
|
|
238
246
|
const retryAfterMs = Math.max(1, live[0] + windowMs - now);
|
|
239
247
|
throw new MiddlewareRateLimitError(maxRequests, windowMs, retryAfterMs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/middleware",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Composable middleware pipeline with composition, timing, error handling, and context propagation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/errors": "1.0
|
|
28
|
+
"@zudojs/errors": "1.2.0"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=24.0.0"
|