@aetherframework/middleware 1.0.2 → 1.0.4
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 +0 -3
- package/docs/readme/README.md +14 -3
- package/docs/readme/README_zh.md +0 -3
- package/examples/advanced-server.js +122 -112
- package/examples/basic-server.js +322 -64
- package/index.js +9 -11
- package/package.json +1 -1
- package/src/core/AetherCompiler.js +117 -63
- package/src/core/AetherContext.js +221 -93
- package/src/core/AetherPipeline.js +261 -285
- package/src/core/AetherRouter.js +358 -256
- package/src/core/AetherStore.js +114 -67
- package/src/middleware/compression.js +165 -91
- package/src/middleware/json.js +180 -169
- package/src/middleware/rate-limit.js +76 -146
- package/src/middleware/security.js +33 -54
- package/src/middleware/session.js +89 -86
package/src/core/AetherRouter.js
CHANGED
|
@@ -1,345 +1,447 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* @license MIT
|
|
3
3
|
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
* @module @aetherframework/middleware/core/AetherRouter
|
|
6
|
+
*
|
|
7
|
+
* Ultra-Optimized Router for Maximum Performance
|
|
8
|
+
* Focus: Pure routing speed with zero overhead
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// [PERF] Pre-allocated frozen objects to prevent heap allocation in hot paths
|
|
12
|
+
const EMPTY_PARAMS = Object.freeze({});
|
|
13
|
+
const EMPTY_QUERY = Object.freeze({});
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
|
-
*
|
|
12
|
-
* Supports versioning, grouping, parameter parsing, and middleware chaining
|
|
16
|
+
* [PERF] Zero-Allocation Query Parser
|
|
13
17
|
*/
|
|
14
|
-
class
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.version = options.version || ""; // API version
|
|
22
|
-
|
|
23
|
-
// Supported HTTP methods
|
|
24
|
-
this.methods = [
|
|
25
|
-
"GET", "POST", "PUT", "DELETE",
|
|
26
|
-
"PATCH", "OPTIONS", "HEAD", "ANY"
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
// Initialize all HTTP method handlers
|
|
30
|
-
this.methods.forEach(method => {
|
|
31
|
-
this[method.toLowerCase()] = this._createRouteHandler(method);
|
|
32
|
-
});
|
|
18
|
+
class JITQueryParser {
|
|
19
|
+
parse(search) {
|
|
20
|
+
if (!search || search.length === 0) return EMPTY_QUERY;
|
|
21
|
+
|
|
22
|
+
const query = {};
|
|
23
|
+
let i = 0;
|
|
24
|
+
const len = search.length;
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
while (i < len) {
|
|
27
|
+
let ampIndex = search.indexOf('&', i);
|
|
28
|
+
if (ampIndex === -1) ampIndex = len;
|
|
29
|
+
|
|
30
|
+
const eqIndex = search.indexOf('=', i);
|
|
31
|
+
|
|
32
|
+
if (eqIndex !== -1 && eqIndex < ampIndex) {
|
|
33
|
+
const rawKey = search.substring(i, eqIndex);
|
|
34
|
+
const rawValue = search.substring(eqIndex + 1, ampIndex);
|
|
35
|
+
const key = decodeURIComponent(rawKey);
|
|
36
|
+
const value = decodeURIComponent(rawValue);
|
|
37
|
+
|
|
38
|
+
const kLen = key.length;
|
|
39
|
+
if (kLen > 2 && key.charCodeAt(kLen - 2) === 91 && key.charCodeAt(kLen - 1) === 93) {
|
|
40
|
+
const arrayKey = key.slice(0, -2);
|
|
41
|
+
let arr = query[arrayKey];
|
|
42
|
+
if (!arr) { arr = []; query[arrayKey] = arr; }
|
|
43
|
+
arr.push(value);
|
|
44
|
+
} else {
|
|
45
|
+
query[key] = value;
|
|
46
|
+
}
|
|
47
|
+
} else if (i < ampIndex) {
|
|
48
|
+
query[decodeURIComponent(search.substring(i, ampIndex))] = "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
i = ampIndex + 1;
|
|
52
|
+
}
|
|
53
|
+
return query;
|
|
36
54
|
}
|
|
55
|
+
}
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
/**
|
|
58
|
+
* [PERF] Optimized Handler Executor with Manual Loop Unrolling
|
|
59
|
+
*/
|
|
60
|
+
class JITHandlerExecutor {
|
|
61
|
+
getExecutor(handlers) {
|
|
62
|
+
const len = handlers.length;
|
|
63
|
+
if (len === 0) return this._exec0;
|
|
64
|
+
if (len === 1) return this._exec1;
|
|
65
|
+
if (len === 2) return this._exec2;
|
|
66
|
+
if (len === 3) return this._exec3;
|
|
67
|
+
if (len === 4) return this._exec4;
|
|
68
|
+
if (len === 5) return this._exec5;
|
|
69
|
+
|
|
70
|
+
return async (context) => {
|
|
71
|
+
let index = 0;
|
|
72
|
+
const next = async () => {
|
|
73
|
+
if (index >= handlers.length || context.isTerminated()) return;
|
|
74
|
+
const handler = handlers[index++];
|
|
75
|
+
try {
|
|
76
|
+
await handler(context, next);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!context.isTerminated()) {
|
|
79
|
+
context.setStatus(500).json({ error: "Internal Server Error" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
51
82
|
};
|
|
52
|
-
|
|
53
|
-
const routeKey = `${method}:${fullPath}`;
|
|
54
|
-
this.routes.set(routeKey, route);
|
|
55
|
-
|
|
56
|
-
this.emit("route:added", { method, path: fullPath, handlers: handlers.length });
|
|
57
|
-
return this;
|
|
83
|
+
await next();
|
|
58
84
|
};
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
async _exec0(ctx) {}
|
|
88
|
+
|
|
89
|
+
// [FIX] 修复了 this 指向问题,this 现在是 handlers 数组
|
|
90
|
+
async _exec1(ctx) {
|
|
91
|
+
if (ctx.isTerminated()) return;
|
|
92
|
+
try { await this[0](ctx, async () => {}); }
|
|
93
|
+
catch (e) { if (!ctx.isTerminated()) ctx.setStatus(500).json({ error: "Internal Server Error" }); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async _exec2(ctx) {
|
|
97
|
+
if (ctx.isTerminated()) return;
|
|
98
|
+
try {
|
|
99
|
+
await this[0](ctx, async () => {
|
|
100
|
+
if (!ctx.isTerminated()) await this[1](ctx, async () => {});
|
|
101
|
+
});
|
|
102
|
+
} catch (e) { if (!ctx.isTerminated()) ctx.setStatus(500).json({ error: "Internal Server Error" }); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async _exec3(ctx) {
|
|
106
|
+
if (ctx.isTerminated()) return;
|
|
107
|
+
try {
|
|
108
|
+
await this[0](ctx, async () => {
|
|
109
|
+
if (!ctx.isTerminated()) await this[1](ctx, async () => {
|
|
110
|
+
if (!ctx.isTerminated()) await this[2](ctx, async () => {});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
} catch (e) { if (!ctx.isTerminated()) ctx.setStatus(500).json({ error: "Internal Server Error" }); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async _exec4(ctx) {
|
|
117
|
+
if (ctx.isTerminated()) return;
|
|
118
|
+
try {
|
|
119
|
+
await this[0](ctx, async () => {
|
|
120
|
+
if (!ctx.isTerminated()) await this[1](ctx, async () => {
|
|
121
|
+
if (!ctx.isTerminated()) await this[2](ctx, async () => {
|
|
122
|
+
if (!ctx.isTerminated()) await this[3](ctx, async () => {});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
} catch (e) { if (!ctx.isTerminated()) ctx.setStatus(500).json({ error: "Internal Server Error" }); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async _exec5(ctx) {
|
|
130
|
+
if (ctx.isTerminated()) return;
|
|
131
|
+
try {
|
|
132
|
+
await this[0](ctx, async () => {
|
|
133
|
+
if (!ctx.isTerminated()) await this[1](ctx, async () => {
|
|
134
|
+
if (!ctx.isTerminated()) await this[2](ctx, async () => {
|
|
135
|
+
if (!ctx.isTerminated()) await this[3](ctx, async () => {
|
|
136
|
+
if (!ctx.isTerminated()) await this[4](ctx, async () => {});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
} catch (e) { if (!ctx.isTerminated()) ctx.setStatus(500).json({ error: "Internal Server Error" }); }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class AetherRouter {
|
|
146
|
+
constructor(options = {}) {
|
|
147
|
+
this.staticRoutes = new Map();
|
|
148
|
+
this.dynamicRoutes = [];
|
|
149
|
+
this.globalMiddlewares = [];
|
|
150
|
+
this.prefixMiddlewares = [];
|
|
151
|
+
this.prefix = options.prefix || "";
|
|
152
|
+
this.version = options.version || "";
|
|
153
|
+
this.routeCache = new Map();
|
|
154
|
+
this.cacheMaxSize = options.cacheMaxSize || 2000;
|
|
155
|
+
this.jitQueryParser = new JITQueryParser();
|
|
156
|
+
this.jitExecutor = new JITHandlerExecutor();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get(path, ...handlers) { return this._addRoute("GET", path, handlers); }
|
|
160
|
+
post(path, ...handlers) { return this._addRoute("POST", path, handlers); }
|
|
161
|
+
put(path, ...handlers) { return this._addRoute("PUT", path, handlers); }
|
|
162
|
+
delete(path, ...handlers) { return this._addRoute("DELETE", path, handlers); }
|
|
163
|
+
patch(path, ...handlers) { return this._addRoute("PATCH", path, handlers); }
|
|
164
|
+
options(path, ...handlers) { return this._addRoute("OPTIONS", path, handlers); }
|
|
165
|
+
head(path, ...handlers) { return this._addRoute("HEAD", path, handlers); }
|
|
166
|
+
all(path, ...handlers) { return this._addRoute("ANY", path, handlers); }
|
|
167
|
+
|
|
168
|
+
_addRoute(method, path, handlers) {
|
|
169
|
+
if (handlers.length === 0) throw new Error(`Route ${method} ${path} must have at least one handler`);
|
|
67
170
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fullPath += `/v${this.version.replace(/[^0-9.]/g, "")}`;
|
|
71
|
-
}
|
|
171
|
+
const fullPath = this._buildPath(path);
|
|
172
|
+
const isStatic = fullPath.indexOf(':') === -1 && fullPath.indexOf('*') === -1;
|
|
72
173
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
fullPath
|
|
76
|
-
|
|
174
|
+
const route = {
|
|
175
|
+
method: method === "ANY" ? null : method,
|
|
176
|
+
path: fullPath,
|
|
177
|
+
isStatic: isStatic,
|
|
178
|
+
handlers: this._wrapHandlers(handlers),
|
|
179
|
+
regex: isStatic ? null : this._pathToRegex(fullPath),
|
|
180
|
+
paramNames: isStatic ? [] : this._extractParamNames(fullPath)
|
|
181
|
+
};
|
|
77
182
|
|
|
78
|
-
|
|
183
|
+
if (isStatic) {
|
|
184
|
+
this.staticRoutes.set(`${route.method || 'ANY'}:${fullPath}`, route);
|
|
185
|
+
} else {
|
|
186
|
+
this.dynamicRoutes.push(route);
|
|
187
|
+
}
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_buildPath(path) {
|
|
192
|
+
let fullPath = "";
|
|
193
|
+
if (this.version) fullPath += `/v${this.version.replace(/[^0-9.]/g, "")}`;
|
|
194
|
+
if (this.prefix) fullPath += `/${this.prefix.replace(/^\/|\/$/g, "")}`;
|
|
79
195
|
fullPath += `/${path.replace(/^\/|\/$/g, "")}`;
|
|
80
|
-
|
|
81
|
-
// Normalize path
|
|
82
196
|
return fullPath.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
83
197
|
}
|
|
84
198
|
|
|
85
|
-
/**
|
|
86
|
-
* Convert path pattern to regex
|
|
87
|
-
* @private
|
|
88
|
-
*/
|
|
89
199
|
_pathToRegex(path) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
.replace(
|
|
93
|
-
.replace(
|
|
94
|
-
.replace(
|
|
95
|
-
|
|
200
|
+
const escapedPath = path.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
201
|
+
const pattern = escapedPath
|
|
202
|
+
.replace(/\\:(\w+)/g, "([^/]+)")
|
|
203
|
+
.replace(/\\*(\w+)?/g, "(.*)")
|
|
204
|
+
.replace(/\\\?/g, "\\?");
|
|
96
205
|
return new RegExp(`^${pattern}$`);
|
|
97
206
|
}
|
|
98
207
|
|
|
99
|
-
|
|
100
|
-
* Extract parameter names from path
|
|
101
|
-
* @private
|
|
102
|
-
*/
|
|
208
|
+
// [FIX] 修复了 push(match) 导致推入整个 RegExp 数组的问题
|
|
103
209
|
_extractParamNames(path) {
|
|
104
210
|
const paramNames = [];
|
|
105
211
|
const paramPattern = /:(\w+)/g;
|
|
106
212
|
const wildcardPattern = /\*(\w+)?/g;
|
|
107
|
-
|
|
108
213
|
let match;
|
|
109
|
-
while ((match = paramPattern.exec(path)) !== null)
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
while ((match = wildcardPattern.exec(path)) !== null) {
|
|
114
|
-
if (match) paramNames.push(match);
|
|
115
|
-
}
|
|
116
|
-
|
|
214
|
+
while ((match = paramPattern.exec(path)) !== null) paramNames.push(match[1]);
|
|
215
|
+
while ((match = wildcardPattern.exec(path)) !== null) if (match[1]) paramNames.push(match[1]);
|
|
117
216
|
return paramNames;
|
|
118
217
|
}
|
|
119
218
|
|
|
120
|
-
/**
|
|
121
|
-
* Wrap handlers with validation
|
|
122
|
-
* @private
|
|
123
|
-
*/
|
|
124
219
|
_wrapHandlers(handlers) {
|
|
125
|
-
|
|
126
|
-
if (typeof
|
|
127
|
-
throw new TypeError(
|
|
220
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
221
|
+
if (typeof handlers[i] !== "function") {
|
|
222
|
+
throw new TypeError(`Route handler must be a function, got ${typeof handlers[i]} at position ${i}`);
|
|
128
223
|
}
|
|
129
|
-
|
|
130
|
-
|
|
224
|
+
}
|
|
225
|
+
return handlers;
|
|
131
226
|
}
|
|
132
227
|
|
|
133
|
-
/**
|
|
134
|
-
* Route grouping
|
|
135
|
-
* @param {string} prefix - Group prefix
|
|
136
|
-
* @param {Function} callback - Group definition function
|
|
137
|
-
*/
|
|
138
228
|
group(prefix, callback) {
|
|
139
|
-
const router = new AetherRouter({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Inherit global middlewares
|
|
145
|
-
router.middlewares = [...this.middlewares];
|
|
146
|
-
|
|
147
|
-
// Execute group definition
|
|
229
|
+
const router = new AetherRouter({ prefix: `${this.prefix}/${prefix}`.replace(/\/+/g, "/"), version: this.version });
|
|
230
|
+
router.globalMiddlewares = this.globalMiddlewares.slice();
|
|
231
|
+
router.prefixMiddlewares = this.prefixMiddlewares.slice();
|
|
148
232
|
callback(router);
|
|
149
233
|
|
|
150
|
-
|
|
151
|
-
router.
|
|
152
|
-
|
|
153
|
-
|
|
234
|
+
router.staticRoutes.forEach((route, key) => this.staticRoutes.set(key, route));
|
|
235
|
+
for (let i = 0; i < router.dynamicRoutes.length; i++) this.dynamicRoutes.push(router.dynamicRoutes[i]);
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
version(version, callback) {
|
|
240
|
+
const router = new AetherRouter({ prefix: this.prefix, version: version });
|
|
241
|
+
router.globalMiddlewares = this.globalMiddlewares.slice();
|
|
242
|
+
router.prefixMiddlewares = this.prefixMiddlewares.slice();
|
|
243
|
+
callback(router);
|
|
154
244
|
|
|
245
|
+
router.staticRoutes.forEach((route, key) => this.staticRoutes.set(key, route));
|
|
246
|
+
for (let i = 0; i < router.dynamicRoutes.length; i++) this.dynamicRoutes.push(router.dynamicRoutes[i]);
|
|
155
247
|
return this;
|
|
156
248
|
}
|
|
157
249
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
250
|
+
// [FIX] 重写了 use 方法,修复了 typeof args 永远为 'object' 的致命判断错误
|
|
251
|
+
use(...args) {
|
|
252
|
+
if (args.length === 0) return this;
|
|
253
|
+
|
|
254
|
+
if (typeof args[0] === 'string') {
|
|
255
|
+
const path = args[0];
|
|
256
|
+
const middlewares = args.slice(1);
|
|
257
|
+
|
|
258
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
259
|
+
let mw = middlewares[i];
|
|
260
|
+
if (typeof mw !== "function") {
|
|
261
|
+
if (mw && typeof mw.middleware === 'function') middlewares[i] = mw.middleware();
|
|
262
|
+
else throw new TypeError(`Middleware for path "${path}" must be a function, got ${typeof mw}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const normalizedPath = path === '/' ? '/' : path.replace(/\/+$/, '');
|
|
267
|
+
const escapedPath = normalizedPath.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
268
|
+
const prefixRegex = normalizedPath === '/' ? null : new RegExp(`^${escapedPath}(?=/|$|\\?)`, 'i');
|
|
269
|
+
|
|
270
|
+
const wrappedMiddlewares = new Array(middlewares.length);
|
|
271
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
272
|
+
const mw = middlewares[i];
|
|
273
|
+
wrappedMiddlewares[i] = async (ctx, next) => {
|
|
274
|
+
if (!prefixRegex) return mw(ctx, next);
|
|
275
|
+
const originalUrl = ctx.url;
|
|
276
|
+
const originalPath = ctx.path;
|
|
277
|
+
if (prefixRegex.test(originalUrl)) {
|
|
278
|
+
let newUrl = originalUrl.replace(prefixRegex, '');
|
|
279
|
+
if (newUrl.charCodeAt(0) !== 47) newUrl = '/' + newUrl;
|
|
280
|
+
ctx.url = newUrl;
|
|
281
|
+
try {
|
|
282
|
+
// [FIX] 修复了 split('?') 返回数组导致 ctx.path 类型错误的问题
|
|
283
|
+
if ('path' in ctx) ctx.path = newUrl.split('?')[0];
|
|
284
|
+
await mw(ctx, next);
|
|
285
|
+
} finally {
|
|
286
|
+
ctx.url = originalUrl;
|
|
287
|
+
if ('path' in ctx) ctx.path = originalPath;
|
|
288
|
+
}
|
|
289
|
+
} else { await next(); }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
182
292
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
293
|
+
if (normalizedPath === '/') {
|
|
294
|
+
this.globalMiddlewares.push(...wrappedMiddlewares);
|
|
295
|
+
} else {
|
|
296
|
+
this.prefixMiddlewares.push({ path: normalizedPath, handlers: wrappedMiddlewares });
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
for (let i = 0; i < args.length; i++) {
|
|
300
|
+
if (typeof args[i] !== "function") throw new TypeError(`Global middleware must be a function, got ${typeof args[i]}`);
|
|
301
|
+
}
|
|
302
|
+
this.globalMiddlewares.push(...args);
|
|
303
|
+
}
|
|
189
304
|
return this;
|
|
190
305
|
}
|
|
191
306
|
|
|
192
|
-
/**
|
|
193
|
-
* Match route for incoming request
|
|
194
|
-
* @param {string} method - HTTP method
|
|
195
|
-
* @param {string} url - Request URL
|
|
196
|
-
* @returns {Object|null} - Matched route info or null
|
|
197
|
-
*/
|
|
198
307
|
match(method, url) {
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
308
|
+
const qIndex = url.indexOf("?");
|
|
309
|
+
const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
|
|
310
|
+
const search = qIndex === -1 ? null : url.substring(qIndex + 1);
|
|
202
311
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
312
|
+
const cacheKey = method + ":" + pathname;
|
|
313
|
+
const cached = this.routeCache.get(cacheKey);
|
|
314
|
+
|
|
315
|
+
if (cached !== undefined) {
|
|
316
|
+
const route = cached.route;
|
|
317
|
+
const params = route && !route.isStatic ? this._extractParams(route, pathname) : EMPTY_PARAMS;
|
|
318
|
+
return {
|
|
319
|
+
route: route,
|
|
320
|
+
params: params,
|
|
321
|
+
query: search ? this.jitQueryParser.parse(search) : EMPTY_QUERY,
|
|
322
|
+
handlers: cached.handlers
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const applicableMiddlewares = [];
|
|
327
|
+
for (let i = 0; i < this.globalMiddlewares.length; i++) applicableMiddlewares.push(this.globalMiddlewares[i]);
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < this.prefixMiddlewares.length; i++) {
|
|
330
|
+
const mw = this.prefixMiddlewares[i];
|
|
331
|
+
const mwPath = mw.path;
|
|
332
|
+
const mwPathSlash = mwPath.endsWith('/') ? mwPath : mwPath + '/';
|
|
333
|
+
if (pathname === mwPath || pathname.startsWith(mwPathSlash)) {
|
|
334
|
+
for (let j = 0; j < mw.handlers.length; j++) applicableMiddlewares.push(mw.handlers[j]);
|
|
210
335
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let matchedRoute = null;
|
|
339
|
+
matchedRoute = this.staticRoutes.get(method + ":" + pathname) || this.staticRoutes.get("ANY:" + pathname);
|
|
340
|
+
|
|
341
|
+
// [FIX] 修复了高并发下 _lastMatch 挂载在共享 route 对象上导致的“串参”竞态条件问题
|
|
342
|
+
let dynamicMatch = null;
|
|
343
|
+
if (!matchedRoute) {
|
|
344
|
+
for (let i = 0; i < this.dynamicRoutes.length; i++) {
|
|
345
|
+
const route = this.dynamicRoutes[i];
|
|
346
|
+
if (route.method !== null && route.method !== method) continue;
|
|
216
347
|
|
|
217
|
-
|
|
218
|
-
if (match
|
|
219
|
-
|
|
348
|
+
const match = route.regex.exec(pathname);
|
|
349
|
+
if (match) {
|
|
350
|
+
matchedRoute = route;
|
|
351
|
+
dynamicMatch = match; // 使用局部变量保存匹配结果
|
|
352
|
+
break;
|
|
220
353
|
}
|
|
221
|
-
|
|
222
|
-
// Extract positional parameters
|
|
223
|
-
route.paramNames.forEach((name, index) => {
|
|
224
|
-
if (!params[name] && match[index + 1]) {
|
|
225
|
-
params[name] = match[index + 1];
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
route,
|
|
231
|
-
params,
|
|
232
|
-
query,
|
|
233
|
-
handlers: [...this.middlewares, ...route.handlers]
|
|
234
|
-
};
|
|
235
354
|
}
|
|
236
355
|
}
|
|
356
|
+
|
|
357
|
+
if (matchedRoute) {
|
|
358
|
+
const finalHandlers = applicableMiddlewares.concat(matchedRoute.handlers);
|
|
359
|
+
if (this.routeCache.size >= this.cacheMaxSize) this.routeCache.clear();
|
|
360
|
+
this.routeCache.set(cacheKey, { route: matchedRoute, handlers: finalHandlers });
|
|
361
|
+
|
|
362
|
+
const params = matchedRoute.isStatic ? EMPTY_PARAMS : this._extractParams(matchedRoute, pathname, dynamicMatch);
|
|
363
|
+
return {
|
|
364
|
+
route: matchedRoute,
|
|
365
|
+
params: params,
|
|
366
|
+
query: search ? this.jitQueryParser.parse(search) : EMPTY_QUERY,
|
|
367
|
+
handlers: finalHandlers
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (applicableMiddlewares.length > 0) {
|
|
372
|
+
if (this.routeCache.size >= this.cacheMaxSize) this.routeCache.clear();
|
|
373
|
+
this.routeCache.set(cacheKey, { route: null, handlers: applicableMiddlewares });
|
|
374
|
+
return {
|
|
375
|
+
route: null,
|
|
376
|
+
params: EMPTY_PARAMS,
|
|
377
|
+
query: search ? this.jitQueryParser.parse(search) : EMPTY_QUERY,
|
|
378
|
+
handlers: applicableMiddlewares
|
|
379
|
+
};
|
|
380
|
+
}
|
|
237
381
|
|
|
238
382
|
return null;
|
|
239
383
|
}
|
|
240
384
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
*/
|
|
245
|
-
_parseQuery(search) {
|
|
246
|
-
const query = {};
|
|
247
|
-
if (!search) return query;
|
|
385
|
+
_extractParams(route, pathname, existingMatch) {
|
|
386
|
+
const match = existingMatch || route.regex.exec(pathname);
|
|
387
|
+
if (!match) return EMPTY_PARAMS;
|
|
248
388
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const decodedKey = decodeURIComponent(key);
|
|
254
|
-
const decodedValue = value ? decodeURIComponent(value) : "";
|
|
255
|
-
|
|
256
|
-
// Support array parameters: key[]=value1&key[]=value2
|
|
257
|
-
if (decodedKey.endsWith("[]")) {
|
|
258
|
-
const arrayKey = decodedKey.slice(0, -2);
|
|
259
|
-
if (!query[arrayKey]) {
|
|
260
|
-
query[arrayKey] = [];
|
|
261
|
-
}
|
|
262
|
-
query[arrayKey].push(decodedValue);
|
|
263
|
-
} else {
|
|
264
|
-
query[decodedKey] = decodedValue;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
389
|
+
const params = {};
|
|
390
|
+
const names = route.paramNames;
|
|
391
|
+
for (let i = 0; i < names.length; i++) {
|
|
392
|
+
params[names[i]] = match[i + 1];
|
|
267
393
|
}
|
|
268
|
-
|
|
269
|
-
return query;
|
|
394
|
+
return params;
|
|
270
395
|
}
|
|
271
396
|
|
|
272
|
-
/**
|
|
273
|
-
* Generate router middleware for AetherPipeline
|
|
274
|
-
*/
|
|
275
397
|
middleware() {
|
|
276
398
|
return async (context, next) => {
|
|
277
399
|
const match = this.match(context.method, context.url);
|
|
278
400
|
|
|
279
401
|
if (match) {
|
|
280
|
-
// Set route parameters and query parameters
|
|
281
402
|
context.params = match.params;
|
|
282
|
-
context.setState("query", match.query);
|
|
283
403
|
context.route = match.route;
|
|
284
404
|
|
|
285
|
-
|
|
286
|
-
|
|
405
|
+
if (typeof context.setState === "function") {
|
|
406
|
+
context.setState("query", match.query);
|
|
407
|
+
} else {
|
|
408
|
+
try { context.query = match.query; } catch (e) { /* Ignore */ }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (match.handlers && match.handlers.length > 0) {
|
|
412
|
+
const executor = this.jitExecutor.getExecutor(match.handlers);
|
|
413
|
+
await executor.call(match.handlers, context);
|
|
414
|
+
} else if (typeof next === "function") {
|
|
415
|
+
await next();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!context.isTerminated() && !match.route && typeof next === "function") {
|
|
419
|
+
await next();
|
|
420
|
+
}
|
|
287
421
|
} else if (typeof next === "function") {
|
|
288
|
-
// No matching route, continue to next middleware
|
|
289
422
|
await next();
|
|
290
423
|
} else {
|
|
291
|
-
|
|
292
|
-
context.setStatus(404).json({
|
|
293
|
-
error: "Not Found",
|
|
294
|
-
message: `Route ${context.method} ${context.url} not found`,
|
|
295
|
-
timestamp: new Date().toISOString()
|
|
296
|
-
});
|
|
424
|
+
context.setStatus(404).json({ error: "Not Found" });
|
|
297
425
|
}
|
|
298
426
|
};
|
|
299
427
|
}
|
|
300
428
|
|
|
301
|
-
/**
|
|
302
|
-
* Execute handler chain
|
|
303
|
-
* @private
|
|
304
|
-
*/
|
|
305
|
-
async _executeHandlers(context, handlers) {
|
|
306
|
-
let index = 0;
|
|
307
|
-
|
|
308
|
-
const executeNext = async () => {
|
|
309
|
-
if (index >= handlers.length || context.isTerminated()) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const handler = handlers[index++];
|
|
314
|
-
await handler(context, executeNext);
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
await executeNext();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Get all registered routes (for debugging)
|
|
322
|
-
*/
|
|
323
429
|
getRoutes() {
|
|
324
430
|
const routes = [];
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
handlers: route.handlers.length
|
|
331
|
-
});
|
|
332
|
-
});
|
|
431
|
+
this.staticRoutes.forEach(r => routes.push({ method: r.method || "ALL", path: r.path, type: 'static' }));
|
|
432
|
+
for (let i = 0; i < this.dynamicRoutes.length; i++) {
|
|
433
|
+
const r = this.dynamicRoutes[i];
|
|
434
|
+
routes.push({ method: r.method || "ALL", path: r.path, type: 'dynamic' });
|
|
435
|
+
}
|
|
333
436
|
return routes;
|
|
334
437
|
}
|
|
335
438
|
|
|
336
|
-
/**
|
|
337
|
-
* Clear all routes and middlewares
|
|
338
|
-
*/
|
|
339
439
|
clear() {
|
|
340
|
-
this.
|
|
341
|
-
this.
|
|
342
|
-
this.
|
|
440
|
+
this.staticRoutes.clear();
|
|
441
|
+
this.dynamicRoutes.length = 0;
|
|
442
|
+
this.globalMiddlewares.length = 0;
|
|
443
|
+
this.prefixMiddlewares.length = 0;
|
|
444
|
+
this.routeCache.clear();
|
|
343
445
|
return this;
|
|
344
446
|
}
|
|
345
447
|
}
|