@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.
Files changed (43) hide show
  1. package/README.md +163 -16
  2. package/dist/middlewareCore/index.d.ts +1 -1
  3. package/dist/middlewareCore/index.js +1 -1
  4. package/dist/middlewareCore/middlewareCore.compose.d.ts +36 -4
  5. package/dist/middlewareCore/middlewareCore.compose.js +53 -14
  6. package/dist/middlewareErrors/index.d.ts +1 -1
  7. package/dist/middlewareErrors/index.js +1 -1
  8. package/dist/middlewareErrors/middlewareError.base.d.ts +30 -0
  9. package/dist/middlewareErrors/middlewareError.base.js +43 -0
  10. package/dist/middlewarePipeline/middlewarePipeline.core.d.ts +3 -1
  11. package/dist/middlewarePipeline/middlewarePipeline.core.js +99 -21
  12. package/dist/middlewareTypes/index.d.ts +1 -1
  13. package/dist/middlewareTypes/middlewareContext.type.d.ts +81 -12
  14. package/dist/middlewareUtils/index.d.ts +1 -1
  15. package/dist/middlewareUtils/index.js +1 -1
  16. package/dist/middlewareUtils/middlewareUtils.builtins.d.ts +87 -8
  17. package/dist/middlewareUtils/middlewareUtils.builtins.js +179 -28
  18. package/package.json +14 -7
  19. package/dist/.tsbuildinfo +0 -1
  20. package/dist/index.d.ts.map +0 -1
  21. package/dist/index.js.map +0 -1
  22. package/dist/middlewareCore/index.d.ts.map +0 -1
  23. package/dist/middlewareCore/index.js.map +0 -1
  24. package/dist/middlewareCore/middlewareCore.compose.d.ts.map +0 -1
  25. package/dist/middlewareCore/middlewareCore.compose.js.map +0 -1
  26. package/dist/middlewareErrors/index.d.ts.map +0 -1
  27. package/dist/middlewareErrors/index.js.map +0 -1
  28. package/dist/middlewareErrors/middlewareError.base.d.ts.map +0 -1
  29. package/dist/middlewareErrors/middlewareError.base.js.map +0 -1
  30. package/dist/middlewarePipeline/index.d.ts.map +0 -1
  31. package/dist/middlewarePipeline/index.js.map +0 -1
  32. package/dist/middlewarePipeline/middlewarePipeline.core.d.ts.map +0 -1
  33. package/dist/middlewarePipeline/middlewarePipeline.core.js.map +0 -1
  34. package/dist/middlewareTypes/index.d.ts.map +0 -1
  35. package/dist/middlewareTypes/index.js.map +0 -1
  36. package/dist/middlewareTypes/middlewareContext.type.d.ts.map +0 -1
  37. package/dist/middlewareTypes/middlewareContext.type.js.map +0 -1
  38. package/dist/middlewareTypes/middlewareDefinition.type.d.ts.map +0 -1
  39. package/dist/middlewareTypes/middlewareDefinition.type.js.map +0 -1
  40. package/dist/middlewareUtils/index.d.ts.map +0 -1
  41. package/dist/middlewareUtils/index.js.map +0 -1
  42. package/dist/middlewareUtils/middlewareUtils.builtins.d.ts.map +0 -1
  43. 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, timing, error handling, and context propagation.
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
- createMiddlewarePipeline,
16
- createTimeoutMiddleware,
15
+ createPipeline,
16
+ timeoutMiddleware,
17
+ loggingMiddleware,
18
+ rateLimitMiddleware,
17
19
  } from "@zudojs/middleware";
18
20
 
19
- const pipeline = createMiddlewarePipeline([
20
- createTimeoutMiddleware(5000),
21
- createLoggingMiddleware(),
22
- createAuthMiddleware(),
23
- ]);
21
+ interface RequestContext {
22
+ readonly method?: string;
23
+ readonly path?: string;
24
+ readonly key?: string;
25
+ }
24
26
 
25
- await pipeline(context, async () => {
26
- return handler(context);
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
- ## Features
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
- - Composable middleware pipeline
33
- - Error handling middleware
34
- - Timing and metrics middleware
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>): (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
- * Sort and filter named middleware by priority.
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
- * Create a middleware that wraps another with timing.
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, void>): NamedMiddleware<TContext, void>;
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
- const MAX_DEPTH = 100;
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
- throw new Error("next() called multiple times");
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
- * Sort and filter named middleware by priority.
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 Sorted and filtered array of middleware handler functions
59
+ * @returns Enabled middleware, in execution order, with names intact
45
60
  */
46
- export function resolveMiddleware(middlewareList) {
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
- * Create a middleware that wraps another with timing.
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 withTiming(name, middleware) {
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
- await middleware(ctx, next);
61
- const duration = performance.now() - start;
62
- if (duration > 100) {
63
- console.warn(`[middleware] ${name} took ${duration.toFixed(1)}ms`);
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 { resolveMiddleware } from "../middlewareCore/middlewareCore.compose.js";
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 = resolveMiddleware(middlewareList);
29
+ const resolved = resolveNamedMiddleware(middlewareList);
18
30
  const maxMiddleware = options?.maxMiddleware ?? DEFAULT_MAX;
19
- const stopOnError = options?.stopOnError ?? true;
31
+ const errorMode = resolveErrorMode(options);
32
+ const signal = options?.signal;
20
33
  if (resolved.length > maxMiddleware) {
21
- throw new Error(`Pipeline has ${resolved.length} middleware, exceeding maximum of ${maxMiddleware}`);
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
- throw new Error("next() called multiple times");
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
- if (i < resolved.length) {
37
- executed.push(enabledNames[i] ?? `middleware-${i}`);
38
- const mw = resolved[i];
39
- return mw(context, () => dispatch(i + 1));
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
- if (stopOnError) {
54
- return {
55
- success: false,
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
- durationMs: performance.now() - startTime,
58
- executedMiddleware: executed,
59
- };
129
+ });
60
130
  }
61
- throw error;
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