@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 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
- Every interpolated field is escaped, so a path containing newlines cannot
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. It is
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` (itself a `BaseError` from
178
- `@zudojs/errors`), so a single `instanceof` catches everything from this
179
- package:
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: `console.warn`. */
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
- * @module middlewareErrors
5
- */
6
- import { BaseError } from "@zudojs/errors";
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
- * `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`.
8
+ * @module middlewareErrors
54
9
  */
55
- export declare class MiddlewareAbortedError extends MiddlewareError {
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
- * @module middlewareErrors
5
- */
6
- import { BaseError, ErrorCode, ErrorCategory, ErrorSeverity, } from "@zudojs/errors";
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
- * `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`.
8
+ * @module middlewareErrors
80
9
  */
81
- export class MiddlewareAbortedError extends MiddlewareError {
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 ?? ((msg) => console.log(msg));
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.1",
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.1"
28
+ "@zudojs/errors": "1.2.0"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=24.0.0"