@gravito/core 1.2.1 → 1.6.1
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 +57 -6
- package/README.zh-TW.md +197 -6
- package/dist/Metrics-VOWWRNNR.js +219 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/compat-CI8hiulX.d.cts +376 -0
- package/dist/compat-CI8hiulX.d.ts +376 -0
- package/dist/compat.cjs +18 -0
- package/dist/compat.d.cts +1 -0
- package/dist/compat.d.ts +1 -0
- package/dist/compat.js +0 -0
- package/dist/engine/index.cjs +1764 -0
- package/dist/engine/index.d.cts +922 -0
- package/dist/engine/index.d.ts +922 -0
- package/dist/engine/index.js +1732 -0
- package/dist/index.cjs +14906 -0
- package/dist/index.d.cts +11008 -0
- package/dist/index.d.ts +11008 -0
- package/dist/index.js +14363 -0
- package/package.json +60 -7
|
@@ -0,0 +1,1732 @@
|
|
|
1
|
+
// src/adapters/bun/RadixNode.ts
|
|
2
|
+
var RadixNode = class _RadixNode {
|
|
3
|
+
// Path segment for this node (e.g., "users", ":id")
|
|
4
|
+
segment;
|
|
5
|
+
// Node type (Static, Param, Wildcard)
|
|
6
|
+
type;
|
|
7
|
+
// Children nodes (mapped by segment for fast lookup)
|
|
8
|
+
children = /* @__PURE__ */ new Map();
|
|
9
|
+
// Specialized child for parameter node (only one per level allowed usually to avoid ambiguity, though some routers support multiple)
|
|
10
|
+
paramChild = null;
|
|
11
|
+
// Specialized child for wildcard node
|
|
12
|
+
wildcardChild = null;
|
|
13
|
+
// Handlers registered at this node (keyed by HTTP method)
|
|
14
|
+
handlers = /* @__PURE__ */ new Map();
|
|
15
|
+
// Parameter name if this is a PARAM node (e.g., "id" for ":id")
|
|
16
|
+
paramName = null;
|
|
17
|
+
// Parameter constraints (regex) - only applicable if this is a PARAM node
|
|
18
|
+
// If we support per-route constraints, they might need to be stored differently,
|
|
19
|
+
// but for now assume constraints are defined at node level (uncommon) or checked at match time.
|
|
20
|
+
// Laravel allows global pattern constraints or per-route.
|
|
21
|
+
// Ideally, constraints should be stored with the handler or part of matching logic.
|
|
22
|
+
// For a Radix tree, if we have constraints, we might need to backtrack if constraint fails?
|
|
23
|
+
// Or simply store constraint with the param node.
|
|
24
|
+
regex = null;
|
|
25
|
+
constructor(segment = "", type = 0 /* STATIC */) {
|
|
26
|
+
this.segment = segment;
|
|
27
|
+
this.type = type;
|
|
28
|
+
}
|
|
29
|
+
toJSON() {
|
|
30
|
+
return {
|
|
31
|
+
segment: this.segment,
|
|
32
|
+
type: this.type,
|
|
33
|
+
children: Array.from(this.children.entries()).map(([k, v]) => [k, v.toJSON()]),
|
|
34
|
+
paramChild: this.paramChild?.toJSON() || null,
|
|
35
|
+
wildcardChild: this.wildcardChild?.toJSON() || null,
|
|
36
|
+
paramName: this.paramName,
|
|
37
|
+
regex: this.regex ? this.regex.source : null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
static fromJSON(json) {
|
|
41
|
+
const node = new _RadixNode(json.segment, json.type);
|
|
42
|
+
node.paramName = json.paramName;
|
|
43
|
+
if (json.regex) {
|
|
44
|
+
node.regex = new RegExp(json.regex);
|
|
45
|
+
}
|
|
46
|
+
if (json.children) {
|
|
47
|
+
for (const [key, childJson] of json.children) {
|
|
48
|
+
node.children.set(key, _RadixNode.fromJSON(childJson));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (json.paramChild) {
|
|
52
|
+
node.paramChild = _RadixNode.fromJSON(json.paramChild);
|
|
53
|
+
}
|
|
54
|
+
if (json.wildcardChild) {
|
|
55
|
+
node.wildcardChild = _RadixNode.fromJSON(json.wildcardChild);
|
|
56
|
+
}
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/adapters/bun/RadixRouter.ts
|
|
62
|
+
var RadixRouter = class _RadixRouter {
|
|
63
|
+
root = new RadixNode();
|
|
64
|
+
// Global parameter constraints (e.g., id => /^\d+$/)
|
|
65
|
+
globalConstraints = /* @__PURE__ */ new Map();
|
|
66
|
+
/**
|
|
67
|
+
* Add a generic parameter constraint
|
|
68
|
+
*/
|
|
69
|
+
where(param, regex) {
|
|
70
|
+
this.globalConstraints.set(param, regex);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register a route
|
|
74
|
+
*/
|
|
75
|
+
add(method, path, handlers) {
|
|
76
|
+
let node = this.root;
|
|
77
|
+
const segments = this.splitPath(path);
|
|
78
|
+
for (let i = 0; i < segments.length; i++) {
|
|
79
|
+
const segment = segments[i];
|
|
80
|
+
if (segment === "*") {
|
|
81
|
+
if (!node.wildcardChild) {
|
|
82
|
+
node.wildcardChild = new RadixNode("*", 2 /* WILDCARD */);
|
|
83
|
+
}
|
|
84
|
+
node = node.wildcardChild;
|
|
85
|
+
break;
|
|
86
|
+
} else if (segment.startsWith(":")) {
|
|
87
|
+
const paramName = segment.slice(1);
|
|
88
|
+
if (!node.paramChild) {
|
|
89
|
+
const child = new RadixNode(segment, 1 /* PARAM */);
|
|
90
|
+
child.paramName = paramName;
|
|
91
|
+
const constraint = this.globalConstraints.get(paramName);
|
|
92
|
+
if (constraint) {
|
|
93
|
+
child.regex = constraint;
|
|
94
|
+
}
|
|
95
|
+
node.paramChild = child;
|
|
96
|
+
}
|
|
97
|
+
node = node.paramChild;
|
|
98
|
+
} else {
|
|
99
|
+
if (!node.children.has(segment)) {
|
|
100
|
+
node.children.set(segment, new RadixNode(segment, 0 /* STATIC */));
|
|
101
|
+
}
|
|
102
|
+
node = node.children.get(segment);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
node.handlers.set(method.toLowerCase(), handlers);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Match a request
|
|
109
|
+
*/
|
|
110
|
+
match(method, path) {
|
|
111
|
+
const normalizedMethod = method.toLowerCase();
|
|
112
|
+
if (path === "/" || path === "") {
|
|
113
|
+
const handlers = this.root.handlers.get(normalizedMethod);
|
|
114
|
+
if (handlers) {
|
|
115
|
+
return { handlers, params: {} };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const searchPath = path.startsWith("/") ? path.slice(1) : path;
|
|
120
|
+
const segments = searchPath.split("/");
|
|
121
|
+
return this.matchRecursive(this.root, segments, 0, {}, normalizedMethod);
|
|
122
|
+
}
|
|
123
|
+
matchRecursive(node, segments, depth, params, method) {
|
|
124
|
+
if (depth >= segments.length) {
|
|
125
|
+
let handlers = node.handlers.get(method);
|
|
126
|
+
if (!handlers) {
|
|
127
|
+
handlers = node.handlers.get("all");
|
|
128
|
+
}
|
|
129
|
+
if (handlers) {
|
|
130
|
+
return { handlers, params };
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const segment = segments[depth];
|
|
135
|
+
const staticChild = node.children.get(segment);
|
|
136
|
+
if (staticChild) {
|
|
137
|
+
const match = this.matchRecursive(staticChild, segments, depth + 1, params, method);
|
|
138
|
+
if (match) {
|
|
139
|
+
return match;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const paramChild = node.paramChild;
|
|
143
|
+
if (paramChild) {
|
|
144
|
+
if (paramChild.regex && !paramChild.regex.test(segment)) {
|
|
145
|
+
} else {
|
|
146
|
+
if (paramChild.paramName) {
|
|
147
|
+
params[paramChild.paramName] = decodeURIComponent(segment);
|
|
148
|
+
const match = this.matchRecursive(paramChild, segments, depth + 1, params, method);
|
|
149
|
+
if (match) {
|
|
150
|
+
return match;
|
|
151
|
+
}
|
|
152
|
+
delete params[paramChild.paramName];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (node.wildcardChild) {
|
|
157
|
+
let handlers = node.wildcardChild.handlers.get(method);
|
|
158
|
+
if (!handlers) {
|
|
159
|
+
handlers = node.wildcardChild.handlers.get("all");
|
|
160
|
+
}
|
|
161
|
+
if (handlers) {
|
|
162
|
+
return { handlers, params };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
splitPath(path) {
|
|
168
|
+
if (path === "/" || path === "") {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
let p = path;
|
|
172
|
+
if (p.startsWith("/")) {
|
|
173
|
+
p = p.slice(1);
|
|
174
|
+
}
|
|
175
|
+
if (p.endsWith("/")) {
|
|
176
|
+
p = p.slice(0, -1);
|
|
177
|
+
}
|
|
178
|
+
return p.split("/");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Serialize the router to a JSON string
|
|
182
|
+
*/
|
|
183
|
+
serialize() {
|
|
184
|
+
return JSON.stringify({
|
|
185
|
+
root: this.root.toJSON(),
|
|
186
|
+
globalConstraints: Array.from(this.globalConstraints.entries()).map(([k, v]) => [
|
|
187
|
+
k,
|
|
188
|
+
v.source
|
|
189
|
+
])
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Restore a router from a serialized JSON string
|
|
194
|
+
*/
|
|
195
|
+
static fromSerialized(json) {
|
|
196
|
+
const data = JSON.parse(json);
|
|
197
|
+
const router = new _RadixRouter();
|
|
198
|
+
router.root = RadixNode.fromJSON(data.root);
|
|
199
|
+
if (data.globalConstraints) {
|
|
200
|
+
for (const [key, source] of data.globalConstraints) {
|
|
201
|
+
router.globalConstraints.set(key, new RegExp(source));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return router;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/engine/AOTRouter.ts
|
|
209
|
+
var AOTRouter = class {
|
|
210
|
+
// Static route cache: "METHOD:PATH" -> RouteMetadata
|
|
211
|
+
/** @internal */
|
|
212
|
+
staticRoutes = /* @__PURE__ */ new Map();
|
|
213
|
+
// Dynamic route handler (Radix Tree)
|
|
214
|
+
dynamicRouter = new RadixRouter();
|
|
215
|
+
// Store all route definitions to support mounting/merging
|
|
216
|
+
/** @internal */
|
|
217
|
+
routeDefinitions = [];
|
|
218
|
+
// Global middleware (applies to all routes)
|
|
219
|
+
/** @internal */
|
|
220
|
+
globalMiddleware = [];
|
|
221
|
+
// Path-based middleware: pattern -> middleware[]
|
|
222
|
+
/** @internal */
|
|
223
|
+
pathMiddleware = /* @__PURE__ */ new Map();
|
|
224
|
+
// Dynamic route patterns: handler function -> route pattern
|
|
225
|
+
// 用於追蹤動態路由的模式,防止高基數問題
|
|
226
|
+
dynamicRoutePatterns = /* @__PURE__ */ new Map();
|
|
227
|
+
middlewareCache = /* @__PURE__ */ new Map();
|
|
228
|
+
cacheMaxSize = 1e3;
|
|
229
|
+
_version = 0;
|
|
230
|
+
/**
|
|
231
|
+
* Get the current version for cache invalidation
|
|
232
|
+
* Incremented whenever middleware or routes are modified
|
|
233
|
+
*/
|
|
234
|
+
get version() {
|
|
235
|
+
return this._version;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Register a route
|
|
239
|
+
*
|
|
240
|
+
* Automatically determines if route is static or dynamic.
|
|
241
|
+
* Static routes are stored in a Map for O(1) lookup.
|
|
242
|
+
* Dynamic routes use the Radix Tree.
|
|
243
|
+
*
|
|
244
|
+
* @param method - HTTP method
|
|
245
|
+
* @param path - Route path
|
|
246
|
+
* @param handler - Route handler
|
|
247
|
+
* @param middleware - Route-specific middleware
|
|
248
|
+
*/
|
|
249
|
+
add(method, path, handler, middleware = []) {
|
|
250
|
+
this.routeDefinitions.push({ method, path, handler, middleware });
|
|
251
|
+
const normalizedMethod = method.toLowerCase();
|
|
252
|
+
if (this.isStaticPath(path)) {
|
|
253
|
+
const key = `${normalizedMethod}:${path}`;
|
|
254
|
+
this.staticRoutes.set(key, { handler, middleware });
|
|
255
|
+
} else {
|
|
256
|
+
const wrappedHandler = handler;
|
|
257
|
+
this.dynamicRouter.add(normalizedMethod, path, [wrappedHandler]);
|
|
258
|
+
this.dynamicRoutePatterns.set(wrappedHandler, path);
|
|
259
|
+
if (middleware.length > 0) {
|
|
260
|
+
this.pathMiddleware.set(`${normalizedMethod}:${path}`, middleware);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Mount another router at a prefix
|
|
266
|
+
*/
|
|
267
|
+
mount(prefix, other) {
|
|
268
|
+
if (other.globalMiddleware.length > 0) {
|
|
269
|
+
this.usePattern(prefix, ...other.globalMiddleware);
|
|
270
|
+
const wildcard = prefix === "/" ? "/*" : `${prefix}/*`;
|
|
271
|
+
this.usePattern(wildcard, ...other.globalMiddleware);
|
|
272
|
+
}
|
|
273
|
+
for (const [pattern, mws] of other.pathMiddleware) {
|
|
274
|
+
if (pattern.includes(":")) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
let newPattern;
|
|
278
|
+
if (pattern === "*") {
|
|
279
|
+
newPattern = prefix === "/" ? "/*" : `${prefix}/*`;
|
|
280
|
+
} else if (pattern.startsWith("/")) {
|
|
281
|
+
newPattern = prefix === "/" ? pattern : `${prefix}${pattern}`;
|
|
282
|
+
} else {
|
|
283
|
+
newPattern = prefix === "/" ? `/${pattern}` : `${prefix}/${pattern}`;
|
|
284
|
+
}
|
|
285
|
+
this.usePattern(newPattern, ...mws);
|
|
286
|
+
}
|
|
287
|
+
for (const def of other.routeDefinitions) {
|
|
288
|
+
let newPath;
|
|
289
|
+
if (prefix === "/") {
|
|
290
|
+
newPath = def.path;
|
|
291
|
+
} else if (def.path === "/") {
|
|
292
|
+
newPath = prefix;
|
|
293
|
+
} else {
|
|
294
|
+
newPath = `${prefix}${def.path}`;
|
|
295
|
+
}
|
|
296
|
+
this.add(def.method, newPath, def.handler, def.middleware);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Add global middleware
|
|
301
|
+
*
|
|
302
|
+
* These run for every request, before route-specific middleware.
|
|
303
|
+
*
|
|
304
|
+
* @param middleware - Middleware functions
|
|
305
|
+
*/
|
|
306
|
+
use(...middleware) {
|
|
307
|
+
this.globalMiddleware.push(...middleware);
|
|
308
|
+
this._version++;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Add path-based middleware
|
|
312
|
+
*
|
|
313
|
+
* Supports wildcard patterns like '/api/*'
|
|
314
|
+
*
|
|
315
|
+
* @param pattern - Path pattern
|
|
316
|
+
* @param middleware - Middleware functions
|
|
317
|
+
*/
|
|
318
|
+
usePattern(pattern, ...middleware) {
|
|
319
|
+
if (pattern === "*") {
|
|
320
|
+
this.globalMiddleware.push(...middleware);
|
|
321
|
+
} else {
|
|
322
|
+
const existing = this.pathMiddleware.get(pattern) ?? [];
|
|
323
|
+
this.pathMiddleware.set(pattern, [...existing, ...middleware]);
|
|
324
|
+
}
|
|
325
|
+
this._version++;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Match a request to a route
|
|
329
|
+
*
|
|
330
|
+
* Returns the handler, params, and all applicable middleware.
|
|
331
|
+
*
|
|
332
|
+
* @param method - HTTP method
|
|
333
|
+
* @param path - Request path
|
|
334
|
+
* @returns Route match or null if not found
|
|
335
|
+
*/
|
|
336
|
+
match(method, path) {
|
|
337
|
+
const normalizedMethod = method.toLowerCase();
|
|
338
|
+
const staticKey = `${normalizedMethod}:${path}`;
|
|
339
|
+
const staticRoute = this.staticRoutes.get(staticKey);
|
|
340
|
+
if (staticRoute) {
|
|
341
|
+
return {
|
|
342
|
+
handler: staticRoute.handler,
|
|
343
|
+
params: {},
|
|
344
|
+
middleware: this.collectMiddleware(path, staticRoute.middleware),
|
|
345
|
+
routePattern: path
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const match = this.dynamicRouter.match(normalizedMethod, path);
|
|
349
|
+
if (match && match.handlers.length > 0) {
|
|
350
|
+
const handler = match.handlers[0];
|
|
351
|
+
const wrappedHandler = match.handlers[0];
|
|
352
|
+
const routePattern = this.dynamicRoutePatterns.get(wrappedHandler);
|
|
353
|
+
const routeKey = routePattern ? `${normalizedMethod}:${routePattern}` : null;
|
|
354
|
+
const routeMiddleware = routeKey ? this.pathMiddleware.get(routeKey) ?? [] : [];
|
|
355
|
+
return {
|
|
356
|
+
handler,
|
|
357
|
+
params: match.params,
|
|
358
|
+
middleware: this.collectMiddleware(path, routeMiddleware),
|
|
359
|
+
routePattern
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
handler: null,
|
|
364
|
+
params: {},
|
|
365
|
+
middleware: []
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Public wrapper for collectMiddleware (used by Gravito for optimization)
|
|
370
|
+
*/
|
|
371
|
+
collectMiddlewarePublic(path, routeMiddleware) {
|
|
372
|
+
return this.collectMiddleware(path, routeMiddleware);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Collect all applicable middleware for a path
|
|
376
|
+
*
|
|
377
|
+
* Order: global -> pattern-based -> route-specific
|
|
378
|
+
*
|
|
379
|
+
* @param path - Request path
|
|
380
|
+
* @param routeMiddleware - Route-specific middleware
|
|
381
|
+
* @returns Combined middleware array
|
|
382
|
+
*/
|
|
383
|
+
collectMiddleware(path, routeMiddleware) {
|
|
384
|
+
if (this.globalMiddleware.length === 0 && this.pathMiddleware.size === 0 && routeMiddleware.length === 0) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
const cacheKey = path;
|
|
388
|
+
const cached = this.middlewareCache.get(cacheKey);
|
|
389
|
+
if (cached !== void 0 && cached.version === this._version) {
|
|
390
|
+
return cached.data;
|
|
391
|
+
}
|
|
392
|
+
const middleware = [];
|
|
393
|
+
if (this.globalMiddleware.length > 0) {
|
|
394
|
+
middleware.push(...this.globalMiddleware);
|
|
395
|
+
}
|
|
396
|
+
if (this.pathMiddleware.size > 0) {
|
|
397
|
+
for (const [pattern, mw] of this.pathMiddleware) {
|
|
398
|
+
if (pattern.includes(":")) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (this.matchPattern(pattern, path)) {
|
|
402
|
+
middleware.push(...mw);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (routeMiddleware.length > 0) {
|
|
407
|
+
middleware.push(...routeMiddleware);
|
|
408
|
+
}
|
|
409
|
+
if (this.middlewareCache.size < this.cacheMaxSize) {
|
|
410
|
+
this.middlewareCache.set(cacheKey, { data: middleware, version: this._version });
|
|
411
|
+
} else if (this.middlewareCache.has(cacheKey)) {
|
|
412
|
+
this.middlewareCache.set(cacheKey, { data: middleware, version: this._version });
|
|
413
|
+
}
|
|
414
|
+
return middleware;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Check if a path is static (no parameters or wildcards)
|
|
418
|
+
*/
|
|
419
|
+
isStaticPath(path) {
|
|
420
|
+
return !path.includes(":") && !path.includes("*");
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Match a pattern against a path
|
|
424
|
+
*
|
|
425
|
+
* Supports:
|
|
426
|
+
* - Exact match: '/api/users'
|
|
427
|
+
* - Wildcard suffix: '/api/*'
|
|
428
|
+
*
|
|
429
|
+
* @param pattern - Pattern to match
|
|
430
|
+
* @param path - Path to test
|
|
431
|
+
* @returns True if pattern matches
|
|
432
|
+
*/
|
|
433
|
+
matchPattern(pattern, path) {
|
|
434
|
+
if (pattern === "*") {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
if (pattern === path) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (pattern.endsWith("/*")) {
|
|
441
|
+
const prefix = pattern.slice(0, -2);
|
|
442
|
+
return path.startsWith(prefix);
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get all registered routes (for debugging)
|
|
448
|
+
*/
|
|
449
|
+
getRoutes() {
|
|
450
|
+
const routes = [];
|
|
451
|
+
for (const key of this.staticRoutes.keys()) {
|
|
452
|
+
const [method, path] = key.split(":");
|
|
453
|
+
routes.push({ method, path, type: "static" });
|
|
454
|
+
}
|
|
455
|
+
return routes;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/engine/analyzer.ts
|
|
460
|
+
function analyzeHandler(handler) {
|
|
461
|
+
const source = handler.toString();
|
|
462
|
+
return {
|
|
463
|
+
usesHeaders: source.includes(".header(") || source.includes(".header)") || source.includes(".headers(") || source.includes(".headers)"),
|
|
464
|
+
usesQuery: source.includes(".query(") || source.includes(".query)") || source.includes(".queries(") || source.includes(".queries)"),
|
|
465
|
+
usesBody: source.includes(".json()") || source.includes(".text()") || source.includes(".formData()") || source.includes(".body"),
|
|
466
|
+
usesParams: source.includes(".param(") || source.includes(".param)") || source.includes(".params(") || source.includes(".params)"),
|
|
467
|
+
isAsync: source.includes("async") || source.includes("await")
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function getOptimalContextType(analysis) {
|
|
471
|
+
if (analysis.usesHeaders) {
|
|
472
|
+
return "fast";
|
|
473
|
+
}
|
|
474
|
+
if (!analysis.usesQuery && !analysis.usesBody && !analysis.usesParams) {
|
|
475
|
+
return "minimal";
|
|
476
|
+
}
|
|
477
|
+
if (!analysis.usesQuery && !analysis.usesBody && analysis.usesParams) {
|
|
478
|
+
return "minimal";
|
|
479
|
+
}
|
|
480
|
+
if (analysis.usesBody) {
|
|
481
|
+
return "full";
|
|
482
|
+
}
|
|
483
|
+
return "fast";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/engine/constants.ts
|
|
487
|
+
var encoder = new TextEncoder();
|
|
488
|
+
var CACHED_RESPONSES = {
|
|
489
|
+
NOT_FOUND: encoder.encode('{"error":"Not Found"}'),
|
|
490
|
+
INTERNAL_ERROR: encoder.encode('{"error":"Internal Server Error"}'),
|
|
491
|
+
OK: encoder.encode('{"ok":true}'),
|
|
492
|
+
EMPTY: new Uint8Array(0)
|
|
493
|
+
};
|
|
494
|
+
var HEADERS = {
|
|
495
|
+
JSON: { "Content-Type": "application/json; charset=utf-8" },
|
|
496
|
+
TEXT: { "Content-Type": "text/plain; charset=utf-8" },
|
|
497
|
+
HTML: { "Content-Type": "text/html; charset=utf-8" }
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/Container/RequestScopeMetrics.ts
|
|
501
|
+
var RequestScopeMetrics = class {
|
|
502
|
+
cleanupStartTime = null;
|
|
503
|
+
cleanupDuration = null;
|
|
504
|
+
scopeSize = 0;
|
|
505
|
+
servicesCleaned = 0;
|
|
506
|
+
errorsOccurred = 0;
|
|
507
|
+
/**
|
|
508
|
+
* Record start of cleanup operation
|
|
509
|
+
*/
|
|
510
|
+
recordCleanupStart() {
|
|
511
|
+
this.cleanupStartTime = performance.now();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Record end of cleanup operation
|
|
515
|
+
*
|
|
516
|
+
* @param scopeSize - Number of services in the scope
|
|
517
|
+
* @param servicesCleaned - Number of services that had cleanup called
|
|
518
|
+
* @param errorsOccurred - Number of cleanup errors
|
|
519
|
+
*/
|
|
520
|
+
recordCleanupEnd(scopeSize, servicesCleaned, errorsOccurred = 0) {
|
|
521
|
+
if (this.cleanupStartTime !== null) {
|
|
522
|
+
this.cleanupDuration = performance.now() - this.cleanupStartTime;
|
|
523
|
+
this.cleanupStartTime = null;
|
|
524
|
+
}
|
|
525
|
+
this.scopeSize = scopeSize;
|
|
526
|
+
this.servicesCleaned = servicesCleaned;
|
|
527
|
+
this.errorsOccurred = errorsOccurred;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get cleanup duration in milliseconds
|
|
531
|
+
*
|
|
532
|
+
* @returns Duration in ms, or null if cleanup not completed
|
|
533
|
+
*/
|
|
534
|
+
getCleanupDuration() {
|
|
535
|
+
return this.cleanupDuration;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Check if cleanup took longer than threshold (default 2ms)
|
|
539
|
+
* Useful for detecting slow cleanups
|
|
540
|
+
*
|
|
541
|
+
* @param thresholdMs - Threshold in milliseconds
|
|
542
|
+
* @returns True if cleanup exceeded threshold
|
|
543
|
+
*/
|
|
544
|
+
isSlowCleanup(thresholdMs = 2) {
|
|
545
|
+
if (this.cleanupDuration === null) return false;
|
|
546
|
+
return this.cleanupDuration > thresholdMs;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Export metrics as JSON for logging/monitoring
|
|
550
|
+
*/
|
|
551
|
+
toJSON() {
|
|
552
|
+
return {
|
|
553
|
+
cleanupDuration: this.cleanupDuration,
|
|
554
|
+
scopeSize: this.scopeSize,
|
|
555
|
+
servicesCleaned: this.servicesCleaned,
|
|
556
|
+
errorsOccurred: this.errorsOccurred,
|
|
557
|
+
hasErrors: this.errorsOccurred > 0,
|
|
558
|
+
isSlowCleanup: this.isSlowCleanup()
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Export metrics as compact string for logging
|
|
563
|
+
*/
|
|
564
|
+
toString() {
|
|
565
|
+
const duration = this.cleanupDuration ?? "pending";
|
|
566
|
+
return `cleanup: ${duration}ms, scope: ${this.scopeSize}, cleaned: ${this.servicesCleaned}, errors: ${this.errorsOccurred}`;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/Container/RequestScopeManager.ts
|
|
571
|
+
var RequestScopeManager = class {
|
|
572
|
+
scoped = /* @__PURE__ */ new Map();
|
|
573
|
+
metadata = /* @__PURE__ */ new Map();
|
|
574
|
+
metrics = new RequestScopeMetrics();
|
|
575
|
+
observer = null;
|
|
576
|
+
constructor(observer) {
|
|
577
|
+
this.observer = observer || null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Set observer for monitoring scope lifecycle
|
|
581
|
+
*/
|
|
582
|
+
setObserver(observer) {
|
|
583
|
+
this.observer = observer;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Get metrics for this scope
|
|
587
|
+
*/
|
|
588
|
+
getMetrics() {
|
|
589
|
+
return this.metrics;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Resolve or retrieve a request-scoped service instance.
|
|
593
|
+
*
|
|
594
|
+
* If the service already exists in this scope, returns the cached instance.
|
|
595
|
+
* Otherwise, calls the factory function to create a new instance and caches it.
|
|
596
|
+
*
|
|
597
|
+
* Automatically detects and records services with cleanup methods.
|
|
598
|
+
*
|
|
599
|
+
* @template T - The type of the service.
|
|
600
|
+
* @param key - The service key (for caching).
|
|
601
|
+
* @param factory - Factory function to create the instance if not cached.
|
|
602
|
+
* @returns The cached or newly created instance.
|
|
603
|
+
*/
|
|
604
|
+
resolve(key, factory) {
|
|
605
|
+
const keyStr = String(key);
|
|
606
|
+
const isFromCache = this.scoped.has(keyStr);
|
|
607
|
+
if (!isFromCache) {
|
|
608
|
+
const instance = factory();
|
|
609
|
+
this.scoped.set(keyStr, instance);
|
|
610
|
+
if (instance && typeof instance === "object" && "cleanup" in instance) {
|
|
611
|
+
this.metadata.set(keyStr, { hasCleanup: true });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
this.observer?.onServiceResolved?.(key, isFromCache);
|
|
615
|
+
return this.scoped.get(keyStr);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Clean up all request-scoped instances.
|
|
619
|
+
*
|
|
620
|
+
* Calls the cleanup() method on each service that has one.
|
|
621
|
+
* Silently ignores cleanup errors to prevent cascading failures.
|
|
622
|
+
* Called automatically by the Gravito engine in the request finally block.
|
|
623
|
+
*
|
|
624
|
+
* @returns Promise that resolves when all cleanup is complete.
|
|
625
|
+
*/
|
|
626
|
+
async cleanup() {
|
|
627
|
+
this.metrics.recordCleanupStart();
|
|
628
|
+
this.observer?.onCleanupStart?.();
|
|
629
|
+
const errors = [];
|
|
630
|
+
let servicesCleaned = 0;
|
|
631
|
+
for (const [, instance] of this.scoped) {
|
|
632
|
+
if (instance && typeof instance === "object" && "cleanup" in instance) {
|
|
633
|
+
const fn = instance.cleanup;
|
|
634
|
+
if (typeof fn === "function") {
|
|
635
|
+
try {
|
|
636
|
+
await fn.call(instance);
|
|
637
|
+
servicesCleaned++;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
errors.push(error);
|
|
640
|
+
this.observer?.onCleanupError?.(
|
|
641
|
+
error instanceof Error ? error : new Error(String(error))
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const scopeSize = this.scoped.size;
|
|
648
|
+
this.scoped.clear();
|
|
649
|
+
this.metadata.clear();
|
|
650
|
+
this.metrics.recordCleanupEnd(scopeSize, servicesCleaned, errors.length);
|
|
651
|
+
this.observer?.onCleanupEnd?.(this.metrics);
|
|
652
|
+
if (errors.length > 0) {
|
|
653
|
+
console.error("RequestScope cleanup errors:", errors);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get the number of services in this scope (for monitoring).
|
|
658
|
+
*
|
|
659
|
+
* @returns The count of cached services.
|
|
660
|
+
*/
|
|
661
|
+
size() {
|
|
662
|
+
return this.scoped.size;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/engine/FastContext.ts
|
|
667
|
+
var FastRequestImpl = class {
|
|
668
|
+
_request;
|
|
669
|
+
_params;
|
|
670
|
+
_path;
|
|
671
|
+
_routePattern;
|
|
672
|
+
_url = null;
|
|
673
|
+
_query = null;
|
|
674
|
+
_headers = null;
|
|
675
|
+
_cachedJson = void 0;
|
|
676
|
+
_jsonParsed = false;
|
|
677
|
+
_cachedText = void 0;
|
|
678
|
+
_textParsed = false;
|
|
679
|
+
_cachedFormData = void 0;
|
|
680
|
+
_formDataParsed = false;
|
|
681
|
+
_cachedQueries = null;
|
|
682
|
+
// Back-reference for release check optimization
|
|
683
|
+
_ctx;
|
|
684
|
+
constructor(ctx) {
|
|
685
|
+
this._ctx = ctx;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Initialize for new request
|
|
689
|
+
*/
|
|
690
|
+
init(request, params = {}, path = "", routePattern) {
|
|
691
|
+
this._request = request;
|
|
692
|
+
this._params = params;
|
|
693
|
+
this._path = path;
|
|
694
|
+
this._routePattern = routePattern;
|
|
695
|
+
this._url = null;
|
|
696
|
+
this._query = null;
|
|
697
|
+
this._headers = null;
|
|
698
|
+
this._cachedJson = void 0;
|
|
699
|
+
this._jsonParsed = false;
|
|
700
|
+
this._cachedText = void 0;
|
|
701
|
+
this._textParsed = false;
|
|
702
|
+
this._cachedFormData = void 0;
|
|
703
|
+
this._formDataParsed = false;
|
|
704
|
+
this._cachedQueries = null;
|
|
705
|
+
return this;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Reset for pooling
|
|
709
|
+
*/
|
|
710
|
+
reset() {
|
|
711
|
+
this._request = void 0;
|
|
712
|
+
this._params = void 0;
|
|
713
|
+
this._url = null;
|
|
714
|
+
this._query = null;
|
|
715
|
+
this._headers = null;
|
|
716
|
+
this._cachedJson = void 0;
|
|
717
|
+
this._jsonParsed = false;
|
|
718
|
+
this._cachedText = void 0;
|
|
719
|
+
this._textParsed = false;
|
|
720
|
+
this._cachedFormData = void 0;
|
|
721
|
+
this._formDataParsed = false;
|
|
722
|
+
this._cachedQueries = null;
|
|
723
|
+
}
|
|
724
|
+
checkReleased() {
|
|
725
|
+
if (this._ctx._isReleased) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
"FastContext usage after release detected! (Object Pool Strict Lifecycle Guard)"
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
get url() {
|
|
732
|
+
this.checkReleased();
|
|
733
|
+
return this._request.url;
|
|
734
|
+
}
|
|
735
|
+
get method() {
|
|
736
|
+
this.checkReleased();
|
|
737
|
+
return this._request.method;
|
|
738
|
+
}
|
|
739
|
+
get path() {
|
|
740
|
+
this.checkReleased();
|
|
741
|
+
return this._path;
|
|
742
|
+
}
|
|
743
|
+
get routePattern() {
|
|
744
|
+
this.checkReleased();
|
|
745
|
+
return this._routePattern;
|
|
746
|
+
}
|
|
747
|
+
param(name) {
|
|
748
|
+
this.checkReleased();
|
|
749
|
+
return this._params[name];
|
|
750
|
+
}
|
|
751
|
+
params() {
|
|
752
|
+
this.checkReleased();
|
|
753
|
+
return { ...this._params };
|
|
754
|
+
}
|
|
755
|
+
getUrl() {
|
|
756
|
+
if (!this._url) {
|
|
757
|
+
this._url = new URL(this._request.url);
|
|
758
|
+
}
|
|
759
|
+
return this._url;
|
|
760
|
+
}
|
|
761
|
+
query(name) {
|
|
762
|
+
this.checkReleased();
|
|
763
|
+
if (!this._query) {
|
|
764
|
+
this._query = this.getUrl().searchParams;
|
|
765
|
+
}
|
|
766
|
+
return this._query.get(name) ?? void 0;
|
|
767
|
+
}
|
|
768
|
+
queries() {
|
|
769
|
+
this.checkReleased();
|
|
770
|
+
if (this._cachedQueries !== null) {
|
|
771
|
+
return this._cachedQueries;
|
|
772
|
+
}
|
|
773
|
+
if (!this._query) {
|
|
774
|
+
this._query = this.getUrl().searchParams;
|
|
775
|
+
}
|
|
776
|
+
const result = {};
|
|
777
|
+
for (const [key, value] of this._query.entries()) {
|
|
778
|
+
const existing = result[key];
|
|
779
|
+
if (existing === void 0) {
|
|
780
|
+
result[key] = value;
|
|
781
|
+
} else if (Array.isArray(existing)) {
|
|
782
|
+
existing.push(value);
|
|
783
|
+
} else {
|
|
784
|
+
result[key] = [existing, value];
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
this._cachedQueries = result;
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
header(name) {
|
|
791
|
+
this.checkReleased();
|
|
792
|
+
return this._request.headers.get(name) ?? void 0;
|
|
793
|
+
}
|
|
794
|
+
headers() {
|
|
795
|
+
this.checkReleased();
|
|
796
|
+
if (!this._headers) {
|
|
797
|
+
this._headers = {};
|
|
798
|
+
for (const [key, value] of this._request.headers.entries()) {
|
|
799
|
+
this._headers[key] = value;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return { ...this._headers };
|
|
803
|
+
}
|
|
804
|
+
async json() {
|
|
805
|
+
this.checkReleased();
|
|
806
|
+
if (!this._jsonParsed) {
|
|
807
|
+
this._cachedJson = await this._request.json();
|
|
808
|
+
this._jsonParsed = true;
|
|
809
|
+
}
|
|
810
|
+
return this._cachedJson;
|
|
811
|
+
}
|
|
812
|
+
async text() {
|
|
813
|
+
this.checkReleased();
|
|
814
|
+
if (!this._textParsed) {
|
|
815
|
+
this._cachedText = await this._request.text();
|
|
816
|
+
this._textParsed = true;
|
|
817
|
+
}
|
|
818
|
+
return this._cachedText;
|
|
819
|
+
}
|
|
820
|
+
async formData() {
|
|
821
|
+
this.checkReleased();
|
|
822
|
+
if (!this._formDataParsed) {
|
|
823
|
+
this._cachedFormData = await this._request.formData();
|
|
824
|
+
this._formDataParsed = true;
|
|
825
|
+
}
|
|
826
|
+
return this._cachedFormData;
|
|
827
|
+
}
|
|
828
|
+
get raw() {
|
|
829
|
+
this.checkReleased();
|
|
830
|
+
return this._request;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
var FastContext = class {
|
|
834
|
+
req = new FastRequestImpl(this);
|
|
835
|
+
// private _statusCode = 200
|
|
836
|
+
_headers = new Headers();
|
|
837
|
+
// Reuse this object
|
|
838
|
+
_isReleased = false;
|
|
839
|
+
// Made public for internal check access
|
|
840
|
+
_requestScope = null;
|
|
841
|
+
// Request-scoped services
|
|
842
|
+
/**
|
|
843
|
+
* Initialize context for a new request
|
|
844
|
+
*
|
|
845
|
+
* This is called when acquiring from the pool.
|
|
846
|
+
*/
|
|
847
|
+
init(request, params = {}, path = "", routePattern) {
|
|
848
|
+
this._isReleased = false;
|
|
849
|
+
this.req.init(request, params, path, routePattern);
|
|
850
|
+
this._headers = new Headers();
|
|
851
|
+
this._requestScope = new RequestScopeManager();
|
|
852
|
+
return this;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Reset context for pooling (Cleanup)
|
|
856
|
+
*
|
|
857
|
+
* This is called when releasing back to the pool.
|
|
858
|
+
* Implements "Deep-Reset Protocol" and "Release Guard".
|
|
859
|
+
*/
|
|
860
|
+
reset() {
|
|
861
|
+
this._isReleased = true;
|
|
862
|
+
this.req.reset();
|
|
863
|
+
this._store.clear();
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Check if context is released
|
|
867
|
+
*/
|
|
868
|
+
checkReleased() {
|
|
869
|
+
if (this._isReleased) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
"FastContext usage after release detected! (Object Pool Strict Lifecycle Guard)"
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
876
|
+
// Response Helpers
|
|
877
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
878
|
+
json(data, status = 200) {
|
|
879
|
+
this.checkReleased();
|
|
880
|
+
this._headers.set("Content-Type", "application/json; charset=utf-8");
|
|
881
|
+
return new Response(JSON.stringify(data), {
|
|
882
|
+
status,
|
|
883
|
+
headers: this._headers
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
text(text, status = 200) {
|
|
887
|
+
this.checkReleased();
|
|
888
|
+
this._headers.set("Content-Type", "text/plain; charset=utf-8");
|
|
889
|
+
return new Response(text, {
|
|
890
|
+
status,
|
|
891
|
+
headers: this._headers
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
html(html, status = 200) {
|
|
895
|
+
this.checkReleased();
|
|
896
|
+
this._headers.set("Content-Type", "text/html; charset=utf-8");
|
|
897
|
+
return new Response(html, {
|
|
898
|
+
status,
|
|
899
|
+
headers: this._headers
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
redirect(url, status = 302) {
|
|
903
|
+
this.checkReleased();
|
|
904
|
+
this._headers.set("Location", url);
|
|
905
|
+
return new Response(null, {
|
|
906
|
+
status,
|
|
907
|
+
headers: this._headers
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
body(data, status = 200) {
|
|
911
|
+
this.checkReleased();
|
|
912
|
+
return new Response(data, {
|
|
913
|
+
status,
|
|
914
|
+
headers: this._headers
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
stream(stream, status = 200) {
|
|
918
|
+
this.checkReleased();
|
|
919
|
+
this._headers.set("Content-Type", "application/octet-stream");
|
|
920
|
+
return new Response(stream, {
|
|
921
|
+
status,
|
|
922
|
+
headers: this._headers
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
notFound(message = "Not Found") {
|
|
926
|
+
return this.text(message, 404);
|
|
927
|
+
}
|
|
928
|
+
forbidden(message = "Forbidden") {
|
|
929
|
+
return this.text(message, 403);
|
|
930
|
+
}
|
|
931
|
+
unauthorized(message = "Unauthorized") {
|
|
932
|
+
return this.text(message, 401);
|
|
933
|
+
}
|
|
934
|
+
badRequest(message = "Bad Request") {
|
|
935
|
+
return this.text(message, 400);
|
|
936
|
+
}
|
|
937
|
+
async forward(target, _options = {}) {
|
|
938
|
+
this.checkReleased();
|
|
939
|
+
const url = new URL(this.req.url);
|
|
940
|
+
const targetUrl = new URL(
|
|
941
|
+
target.startsWith("http") ? target : `${url.protocol}//${target}${this.req.path}`
|
|
942
|
+
);
|
|
943
|
+
const searchParams = new URLSearchParams(url.search);
|
|
944
|
+
searchParams.forEach((v, k) => {
|
|
945
|
+
targetUrl.searchParams.set(k, v);
|
|
946
|
+
});
|
|
947
|
+
return fetch(targetUrl.toString(), {
|
|
948
|
+
method: this.req.method,
|
|
949
|
+
headers: this.req.raw.headers,
|
|
950
|
+
body: this.req.method !== "GET" && this.req.method !== "HEAD" ? this.req.raw.body : null,
|
|
951
|
+
// @ts-expect-error - Bun/Fetch specific
|
|
952
|
+
duplex: "half"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
header(name, value) {
|
|
956
|
+
this.checkReleased();
|
|
957
|
+
if (value !== void 0) {
|
|
958
|
+
this._headers.set(name, value);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
return this.req.header(name);
|
|
962
|
+
}
|
|
963
|
+
status(_code) {
|
|
964
|
+
this.checkReleased();
|
|
965
|
+
}
|
|
966
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
967
|
+
// Context Variables
|
|
968
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
969
|
+
_store = /* @__PURE__ */ new Map();
|
|
970
|
+
get(key) {
|
|
971
|
+
return this._store.get(key);
|
|
972
|
+
}
|
|
973
|
+
set(key, value) {
|
|
974
|
+
this._store.set(key, value);
|
|
975
|
+
}
|
|
976
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
977
|
+
// Request Scope Management
|
|
978
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
979
|
+
/**
|
|
980
|
+
* Get the request-scoped service manager for this request.
|
|
981
|
+
*
|
|
982
|
+
* @returns The RequestScopeManager for this request.
|
|
983
|
+
* @throws Error if called before init() or after reset().
|
|
984
|
+
*/
|
|
985
|
+
requestScope() {
|
|
986
|
+
if (!this._requestScope) {
|
|
987
|
+
throw new Error("RequestScope not initialized. Call init() first.");
|
|
988
|
+
}
|
|
989
|
+
return this._requestScope;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Resolve a request-scoped service (convenience method).
|
|
993
|
+
*
|
|
994
|
+
* @template T - The service type.
|
|
995
|
+
* @param key - The service key for caching.
|
|
996
|
+
* @param factory - Factory function to create the service.
|
|
997
|
+
* @returns The cached or newly created service instance.
|
|
998
|
+
*/
|
|
999
|
+
scoped(key, factory) {
|
|
1000
|
+
return this.requestScope().resolve(key, factory);
|
|
1001
|
+
}
|
|
1002
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1003
|
+
// Lifecycle helpers
|
|
1004
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1005
|
+
route = () => "";
|
|
1006
|
+
get native() {
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// src/engine/MinimalContext.ts
|
|
1012
|
+
var MinimalRequest = class {
|
|
1013
|
+
constructor(_request, _params, _path, _routePattern) {
|
|
1014
|
+
this._request = _request;
|
|
1015
|
+
this._params = _params;
|
|
1016
|
+
this._path = _path;
|
|
1017
|
+
this._routePattern = _routePattern;
|
|
1018
|
+
}
|
|
1019
|
+
_searchParams = null;
|
|
1020
|
+
_cachedQueries = null;
|
|
1021
|
+
_cachedJsonPromise = null;
|
|
1022
|
+
_cachedTextPromise = null;
|
|
1023
|
+
_cachedFormDataPromise = null;
|
|
1024
|
+
get url() {
|
|
1025
|
+
return this._request.url;
|
|
1026
|
+
}
|
|
1027
|
+
get method() {
|
|
1028
|
+
return this._request.method;
|
|
1029
|
+
}
|
|
1030
|
+
get path() {
|
|
1031
|
+
return this._path;
|
|
1032
|
+
}
|
|
1033
|
+
get routePattern() {
|
|
1034
|
+
return this._routePattern;
|
|
1035
|
+
}
|
|
1036
|
+
param(name) {
|
|
1037
|
+
return this._params[name];
|
|
1038
|
+
}
|
|
1039
|
+
params() {
|
|
1040
|
+
return { ...this._params };
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Lazy-initialize searchParams, only parse once
|
|
1044
|
+
*/
|
|
1045
|
+
getSearchParams() {
|
|
1046
|
+
if (this._searchParams === null) {
|
|
1047
|
+
const url = this._request.url;
|
|
1048
|
+
const queryStart = url.indexOf("?");
|
|
1049
|
+
if (queryStart === -1) {
|
|
1050
|
+
this._searchParams = new URLSearchParams();
|
|
1051
|
+
} else {
|
|
1052
|
+
const hashStart = url.indexOf("#", queryStart);
|
|
1053
|
+
const queryString = hashStart === -1 ? url.slice(queryStart + 1) : url.slice(queryStart + 1, hashStart);
|
|
1054
|
+
this._searchParams = new URLSearchParams(queryString);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return this._searchParams;
|
|
1058
|
+
}
|
|
1059
|
+
query(name) {
|
|
1060
|
+
return this.getSearchParams().get(name) ?? void 0;
|
|
1061
|
+
}
|
|
1062
|
+
queries() {
|
|
1063
|
+
if (this._cachedQueries !== null) {
|
|
1064
|
+
return this._cachedQueries;
|
|
1065
|
+
}
|
|
1066
|
+
const params = this.getSearchParams();
|
|
1067
|
+
const result = {};
|
|
1068
|
+
for (const [key, value] of params.entries()) {
|
|
1069
|
+
const existing = result[key];
|
|
1070
|
+
if (existing === void 0) {
|
|
1071
|
+
result[key] = value;
|
|
1072
|
+
} else if (Array.isArray(existing)) {
|
|
1073
|
+
existing.push(value);
|
|
1074
|
+
} else {
|
|
1075
|
+
result[key] = [existing, value];
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
this._cachedQueries = result;
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
header(name) {
|
|
1082
|
+
return this._request.headers.get(name) ?? void 0;
|
|
1083
|
+
}
|
|
1084
|
+
headers() {
|
|
1085
|
+
const result = {};
|
|
1086
|
+
for (const [key, value] of this._request.headers.entries()) {
|
|
1087
|
+
result[key] = value;
|
|
1088
|
+
}
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
async json() {
|
|
1092
|
+
if (this._cachedJsonPromise === null) {
|
|
1093
|
+
this._cachedJsonPromise = this._request.json();
|
|
1094
|
+
}
|
|
1095
|
+
return this._cachedJsonPromise;
|
|
1096
|
+
}
|
|
1097
|
+
async text() {
|
|
1098
|
+
if (this._cachedTextPromise === null) {
|
|
1099
|
+
this._cachedTextPromise = this._request.text();
|
|
1100
|
+
}
|
|
1101
|
+
return this._cachedTextPromise;
|
|
1102
|
+
}
|
|
1103
|
+
async formData() {
|
|
1104
|
+
if (this._cachedFormDataPromise === null) {
|
|
1105
|
+
this._cachedFormDataPromise = this._request.formData();
|
|
1106
|
+
}
|
|
1107
|
+
return this._cachedFormDataPromise;
|
|
1108
|
+
}
|
|
1109
|
+
get raw() {
|
|
1110
|
+
return this._request;
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
var MinimalContext = class {
|
|
1114
|
+
req;
|
|
1115
|
+
_resHeaders = {};
|
|
1116
|
+
_requestScope;
|
|
1117
|
+
constructor(request, params, path, routePattern) {
|
|
1118
|
+
this.req = new MinimalRequest(request, params, path, routePattern);
|
|
1119
|
+
this._requestScope = new RequestScopeManager();
|
|
1120
|
+
}
|
|
1121
|
+
// get req(): FastRequest {
|
|
1122
|
+
// return this._req
|
|
1123
|
+
// }
|
|
1124
|
+
// Response helpers - merge custom headers with defaults
|
|
1125
|
+
// Optimized: use Object.assign instead of spread to avoid shallow copy overhead
|
|
1126
|
+
getHeaders(contentType) {
|
|
1127
|
+
const headers = Object.assign({ "Content-Type": contentType }, this._resHeaders);
|
|
1128
|
+
return headers;
|
|
1129
|
+
}
|
|
1130
|
+
json(data, status = 200) {
|
|
1131
|
+
return new Response(JSON.stringify(data), {
|
|
1132
|
+
status,
|
|
1133
|
+
headers: this.getHeaders("application/json; charset=utf-8")
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
text(text, status = 200) {
|
|
1137
|
+
return new Response(text, {
|
|
1138
|
+
status,
|
|
1139
|
+
headers: this.getHeaders("text/plain; charset=utf-8")
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
html(html, status = 200) {
|
|
1143
|
+
return new Response(html, {
|
|
1144
|
+
status,
|
|
1145
|
+
headers: this.getHeaders("text/html; charset=utf-8")
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
redirect(url, status = 302) {
|
|
1149
|
+
return new Response(null, {
|
|
1150
|
+
status,
|
|
1151
|
+
headers: { ...this._resHeaders, Location: url }
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
body(data, status = 200) {
|
|
1155
|
+
return new Response(data, {
|
|
1156
|
+
status,
|
|
1157
|
+
headers: this._resHeaders
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
header(name, value) {
|
|
1161
|
+
if (value !== void 0) {
|
|
1162
|
+
this._resHeaders[name] = value;
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
return this.req.header(name);
|
|
1166
|
+
}
|
|
1167
|
+
status(_code) {
|
|
1168
|
+
}
|
|
1169
|
+
stream(stream, status = 200) {
|
|
1170
|
+
return new Response(stream, {
|
|
1171
|
+
status,
|
|
1172
|
+
headers: this.getHeaders("application/octet-stream")
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
notFound(message = "Not Found") {
|
|
1176
|
+
return this.text(message, 404);
|
|
1177
|
+
}
|
|
1178
|
+
forbidden(message = "Forbidden") {
|
|
1179
|
+
return this.text(message, 403);
|
|
1180
|
+
}
|
|
1181
|
+
unauthorized(message = "Unauthorized") {
|
|
1182
|
+
return this.text(message, 401);
|
|
1183
|
+
}
|
|
1184
|
+
badRequest(message = "Bad Request") {
|
|
1185
|
+
return this.text(message, 400);
|
|
1186
|
+
}
|
|
1187
|
+
async forward(target, _options = {}) {
|
|
1188
|
+
const url = new URL(this.req.url);
|
|
1189
|
+
const targetUrl = new URL(
|
|
1190
|
+
target.startsWith("http") ? target : `${url.protocol}//${target}${this.req.path}`
|
|
1191
|
+
);
|
|
1192
|
+
return fetch(targetUrl.toString(), {
|
|
1193
|
+
method: this.req.method,
|
|
1194
|
+
headers: this.req.raw.headers
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
get(_key) {
|
|
1198
|
+
return void 0;
|
|
1199
|
+
}
|
|
1200
|
+
set(_key, _value) {
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Get the request-scoped service manager for this request.
|
|
1204
|
+
*
|
|
1205
|
+
* @returns The RequestScopeManager for this request.
|
|
1206
|
+
*/
|
|
1207
|
+
requestScope() {
|
|
1208
|
+
return this._requestScope;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Resolve a request-scoped service (convenience method).
|
|
1212
|
+
*
|
|
1213
|
+
* @template T - The service type.
|
|
1214
|
+
* @param key - The service key for caching.
|
|
1215
|
+
* @param factory - Factory function to create the service.
|
|
1216
|
+
* @returns The cached or newly created service instance.
|
|
1217
|
+
*/
|
|
1218
|
+
scoped(key, factory) {
|
|
1219
|
+
return this._requestScope.resolve(key, factory);
|
|
1220
|
+
}
|
|
1221
|
+
route = () => "";
|
|
1222
|
+
get native() {
|
|
1223
|
+
return this;
|
|
1224
|
+
}
|
|
1225
|
+
// Required for interface compatibility
|
|
1226
|
+
init(_request, _params, _path) {
|
|
1227
|
+
throw new Error("MinimalContext does not support init. Create a new instance instead.");
|
|
1228
|
+
}
|
|
1229
|
+
// Required for interface compatibility
|
|
1230
|
+
reset() {
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
// src/engine/path.ts
|
|
1235
|
+
function extractPath(url) {
|
|
1236
|
+
const protocolEnd = url.indexOf("://");
|
|
1237
|
+
const searchStart = protocolEnd === -1 ? 0 : protocolEnd + 3;
|
|
1238
|
+
const pathStart = url.indexOf("/", searchStart);
|
|
1239
|
+
if (pathStart === -1) {
|
|
1240
|
+
return "/";
|
|
1241
|
+
}
|
|
1242
|
+
const queryStart = url.indexOf("?", pathStart);
|
|
1243
|
+
if (queryStart === -1) {
|
|
1244
|
+
return url.slice(pathStart);
|
|
1245
|
+
}
|
|
1246
|
+
return url.slice(pathStart, queryStart);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/engine/pool.ts
|
|
1250
|
+
var ObjectPool = class {
|
|
1251
|
+
pool = [];
|
|
1252
|
+
factory;
|
|
1253
|
+
reset;
|
|
1254
|
+
maxSize;
|
|
1255
|
+
/**
|
|
1256
|
+
* Create a new object pool
|
|
1257
|
+
*
|
|
1258
|
+
* @param factory - Function to create new objects
|
|
1259
|
+
* @param reset - Function to reset objects before reuse
|
|
1260
|
+
* @param maxSize - Maximum pool size (default: 256)
|
|
1261
|
+
*/
|
|
1262
|
+
constructor(factory, reset, maxSize = 256) {
|
|
1263
|
+
this.factory = factory;
|
|
1264
|
+
this.reset = reset;
|
|
1265
|
+
this.maxSize = maxSize;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Acquire an object from the pool
|
|
1269
|
+
*
|
|
1270
|
+
* If the pool is empty, creates a new object (overflow strategy).
|
|
1271
|
+
* This ensures the pool never blocks under high load.
|
|
1272
|
+
*
|
|
1273
|
+
* @returns Object from pool or newly created
|
|
1274
|
+
*/
|
|
1275
|
+
acquire() {
|
|
1276
|
+
const obj = this.pool.pop();
|
|
1277
|
+
if (obj !== void 0) {
|
|
1278
|
+
return obj;
|
|
1279
|
+
}
|
|
1280
|
+
return this.factory();
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Release an object back to the pool
|
|
1284
|
+
*
|
|
1285
|
+
* If the pool is full, the object is discarded (will be GC'd).
|
|
1286
|
+
* This prevents unbounded memory growth.
|
|
1287
|
+
*
|
|
1288
|
+
* @param obj - Object to release
|
|
1289
|
+
*/
|
|
1290
|
+
release(obj) {
|
|
1291
|
+
if (this.pool.length < this.maxSize) {
|
|
1292
|
+
this.reset(obj);
|
|
1293
|
+
this.pool.push(obj);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Clear all objects from the pool
|
|
1298
|
+
*
|
|
1299
|
+
* Useful for testing or when you need to force a clean slate.
|
|
1300
|
+
*/
|
|
1301
|
+
clear() {
|
|
1302
|
+
this.pool = [];
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Get current pool size
|
|
1306
|
+
*/
|
|
1307
|
+
get size() {
|
|
1308
|
+
return this.pool.length;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Get maximum pool size
|
|
1312
|
+
*/
|
|
1313
|
+
get capacity() {
|
|
1314
|
+
return this.maxSize;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Pre-warm the pool by creating objects in advance
|
|
1318
|
+
*
|
|
1319
|
+
* This can reduce latency for the first N requests.
|
|
1320
|
+
*
|
|
1321
|
+
* @param count - Number of objects to pre-create
|
|
1322
|
+
*/
|
|
1323
|
+
prewarm(count) {
|
|
1324
|
+
const targetSize = Math.min(count, this.maxSize);
|
|
1325
|
+
while (this.pool.length < targetSize) {
|
|
1326
|
+
const obj = this.factory();
|
|
1327
|
+
this.reset(obj);
|
|
1328
|
+
this.pool.push(obj);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
// src/engine/Gravito.ts
|
|
1334
|
+
function compileMiddlewareChain(middleware, handler) {
|
|
1335
|
+
if (middleware.length === 0) {
|
|
1336
|
+
return handler;
|
|
1337
|
+
}
|
|
1338
|
+
if (middleware.length === 1) {
|
|
1339
|
+
const mw = middleware[0];
|
|
1340
|
+
return async (ctx) => {
|
|
1341
|
+
let nextCalled = false;
|
|
1342
|
+
const result = await mw(ctx, async () => {
|
|
1343
|
+
nextCalled = true;
|
|
1344
|
+
return void 0;
|
|
1345
|
+
});
|
|
1346
|
+
if (result instanceof Response) {
|
|
1347
|
+
return result;
|
|
1348
|
+
}
|
|
1349
|
+
if (nextCalled) {
|
|
1350
|
+
return await handler(ctx);
|
|
1351
|
+
}
|
|
1352
|
+
return ctx.json({ error: "Middleware did not call next or return response" }, 500);
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
let compiled = handler;
|
|
1356
|
+
for (let i = middleware.length - 1; i >= 0; i--) {
|
|
1357
|
+
const mw = middleware[i];
|
|
1358
|
+
const nextHandler = compiled;
|
|
1359
|
+
compiled = async (ctx) => {
|
|
1360
|
+
let nextCalled = false;
|
|
1361
|
+
const result = await mw(ctx, async () => {
|
|
1362
|
+
nextCalled = true;
|
|
1363
|
+
return void 0;
|
|
1364
|
+
});
|
|
1365
|
+
if (result instanceof Response) {
|
|
1366
|
+
return result;
|
|
1367
|
+
}
|
|
1368
|
+
if (nextCalled) {
|
|
1369
|
+
return await nextHandler(ctx);
|
|
1370
|
+
}
|
|
1371
|
+
return ctx.json({ error: "Middleware did not call next or return response" }, 500);
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return compiled;
|
|
1375
|
+
}
|
|
1376
|
+
var Gravito = class {
|
|
1377
|
+
router = new AOTRouter();
|
|
1378
|
+
contextPool;
|
|
1379
|
+
errorHandler;
|
|
1380
|
+
notFoundHandler;
|
|
1381
|
+
// Direct reference to static routes Map (O(1) access)
|
|
1382
|
+
/** @internal */
|
|
1383
|
+
staticRoutes;
|
|
1384
|
+
// Flag: pure static app (no middleware at all) allows ultra-fast path
|
|
1385
|
+
isPureStaticApp = true;
|
|
1386
|
+
// Cache for precompiled dynamic routes
|
|
1387
|
+
compiledDynamicRoutes = /* @__PURE__ */ new Map();
|
|
1388
|
+
/**
|
|
1389
|
+
* Create a new Gravito instance
|
|
1390
|
+
*
|
|
1391
|
+
* @param options - Engine configuration options
|
|
1392
|
+
*/
|
|
1393
|
+
constructor(options = {}) {
|
|
1394
|
+
const poolSize = options.poolSize ?? 256;
|
|
1395
|
+
this.contextPool = new ObjectPool(
|
|
1396
|
+
() => new FastContext(),
|
|
1397
|
+
(ctx) => ctx.reset(),
|
|
1398
|
+
poolSize
|
|
1399
|
+
);
|
|
1400
|
+
this.contextPool.prewarm(Math.min(32, poolSize));
|
|
1401
|
+
if (options.onError) {
|
|
1402
|
+
this.errorHandler = options.onError;
|
|
1403
|
+
}
|
|
1404
|
+
if (options.onNotFound) {
|
|
1405
|
+
this.notFoundHandler = options.onNotFound;
|
|
1406
|
+
}
|
|
1407
|
+
this.compileRoutes();
|
|
1408
|
+
}
|
|
1409
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1410
|
+
// HTTP Method Registration
|
|
1411
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1412
|
+
/**
|
|
1413
|
+
* Register a GET route
|
|
1414
|
+
*
|
|
1415
|
+
* @param path - Route path (e.g., '/users/:id')
|
|
1416
|
+
* @param handlers - Handler and optional middleware
|
|
1417
|
+
* @returns This instance for chaining
|
|
1418
|
+
*/
|
|
1419
|
+
get(path, ...handlers) {
|
|
1420
|
+
return this.addRoute("get", path, handlers);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Register a POST route
|
|
1424
|
+
*/
|
|
1425
|
+
post(path, ...handlers) {
|
|
1426
|
+
return this.addRoute("post", path, handlers);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Register a PUT route
|
|
1430
|
+
*/
|
|
1431
|
+
put(path, ...handlers) {
|
|
1432
|
+
return this.addRoute("put", path, handlers);
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Register a DELETE route
|
|
1436
|
+
*/
|
|
1437
|
+
delete(path, ...handlers) {
|
|
1438
|
+
return this.addRoute("delete", path, handlers);
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Register a PDF route
|
|
1442
|
+
*/
|
|
1443
|
+
patch(path, ...handlers) {
|
|
1444
|
+
return this.addRoute("patch", path, handlers);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Register an OPTIONS route
|
|
1448
|
+
*/
|
|
1449
|
+
options(path, ...handlers) {
|
|
1450
|
+
return this.addRoute("options", path, handlers);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Register a HEAD route
|
|
1454
|
+
*/
|
|
1455
|
+
head(path, ...handlers) {
|
|
1456
|
+
return this.addRoute("head", path, handlers);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Register a route for all HTTP methods
|
|
1460
|
+
*/
|
|
1461
|
+
all(path, ...handlers) {
|
|
1462
|
+
const methods = ["get", "post", "put", "delete", "patch", "options", "head"];
|
|
1463
|
+
for (const method of methods) {
|
|
1464
|
+
this.addRoute(method, path, handlers);
|
|
1465
|
+
}
|
|
1466
|
+
return this;
|
|
1467
|
+
}
|
|
1468
|
+
use(pathOrMiddleware, ...middleware) {
|
|
1469
|
+
this.isPureStaticApp = false;
|
|
1470
|
+
if (typeof pathOrMiddleware === "string") {
|
|
1471
|
+
this.router.usePattern(pathOrMiddleware, ...middleware);
|
|
1472
|
+
} else {
|
|
1473
|
+
this.router.use(pathOrMiddleware, ...middleware);
|
|
1474
|
+
}
|
|
1475
|
+
this.compileRoutes();
|
|
1476
|
+
return this;
|
|
1477
|
+
}
|
|
1478
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1479
|
+
// Route Grouping
|
|
1480
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1481
|
+
/**
|
|
1482
|
+
* Mount a sub-application at a path prefix
|
|
1483
|
+
*/
|
|
1484
|
+
route(path, app) {
|
|
1485
|
+
this.router.mount(path, app.router);
|
|
1486
|
+
this.compileRoutes();
|
|
1487
|
+
return this;
|
|
1488
|
+
}
|
|
1489
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1490
|
+
// Error Handling
|
|
1491
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1492
|
+
/**
|
|
1493
|
+
* Set custom error handler
|
|
1494
|
+
*/
|
|
1495
|
+
onError(handler) {
|
|
1496
|
+
this.errorHandler = handler;
|
|
1497
|
+
return this;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Set custom 404 handler
|
|
1501
|
+
*/
|
|
1502
|
+
notFound(handler) {
|
|
1503
|
+
this.notFoundHandler = handler;
|
|
1504
|
+
return this;
|
|
1505
|
+
}
|
|
1506
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1507
|
+
// Request Handling (Bun.serve integration)
|
|
1508
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1509
|
+
/**
|
|
1510
|
+
* Predictive Route Warming (JIT Optimization)
|
|
1511
|
+
*
|
|
1512
|
+
* Simulates requests to specified routes to trigger JIT compilation (FTL)
|
|
1513
|
+
* before real traffic arrives.
|
|
1514
|
+
*
|
|
1515
|
+
* @param paths List of paths to warm up (e.g. ['/api/users', '/health'])
|
|
1516
|
+
*/
|
|
1517
|
+
async warmup(paths) {
|
|
1518
|
+
const dummyReqOpts = { headers: { "User-Agent": "Gravito-Warmup/1.0" } };
|
|
1519
|
+
for (const path of paths) {
|
|
1520
|
+
const req = new Request(`http://localhost${path}`, dummyReqOpts);
|
|
1521
|
+
await this.fetch(req);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Handle an incoming request
|
|
1526
|
+
*/
|
|
1527
|
+
fetch = async (request) => {
|
|
1528
|
+
const path = extractPath(request.url);
|
|
1529
|
+
const method = request.method.toLowerCase();
|
|
1530
|
+
const staticKey = `${method}:${path}`;
|
|
1531
|
+
const staticRoute = this.staticRoutes.get(staticKey);
|
|
1532
|
+
if (staticRoute) {
|
|
1533
|
+
if (staticRoute.useMinimal) {
|
|
1534
|
+
const ctx = new MinimalContext(request, {}, path, path);
|
|
1535
|
+
try {
|
|
1536
|
+
const result = staticRoute.handler(ctx);
|
|
1537
|
+
if (result instanceof Response) {
|
|
1538
|
+
return result;
|
|
1539
|
+
}
|
|
1540
|
+
return await result;
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
return this.handleErrorSync(error, request, path);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return await this.handleWithMiddleware(request, path, staticRoute);
|
|
1546
|
+
}
|
|
1547
|
+
return await this.handleDynamicRoute(request, method, path);
|
|
1548
|
+
};
|
|
1549
|
+
/**
|
|
1550
|
+
* Handle routes with middleware (async path)
|
|
1551
|
+
*/
|
|
1552
|
+
async handleWithMiddleware(request, path, route) {
|
|
1553
|
+
const ctx = this.contextPool.acquire();
|
|
1554
|
+
try {
|
|
1555
|
+
ctx.init(request, {}, path, path);
|
|
1556
|
+
if (route.compiled) {
|
|
1557
|
+
return await route.compiled(ctx);
|
|
1558
|
+
}
|
|
1559
|
+
const middleware = this.collectMiddlewareForPath(path, route.middleware);
|
|
1560
|
+
if (middleware.length === 0) {
|
|
1561
|
+
return await route.handler(ctx);
|
|
1562
|
+
}
|
|
1563
|
+
return await this.executeMiddleware(ctx, middleware, route.handler);
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
return await this.handleError(error, ctx);
|
|
1566
|
+
} finally {
|
|
1567
|
+
try {
|
|
1568
|
+
await ctx.requestScope().cleanup();
|
|
1569
|
+
} catch (cleanupError) {
|
|
1570
|
+
console.error("RequestScope cleanup failed:", cleanupError);
|
|
1571
|
+
}
|
|
1572
|
+
this.contextPool.release(ctx);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Handle dynamic routes (Radix Tree lookup)
|
|
1577
|
+
*/
|
|
1578
|
+
handleDynamicRoute(request, method, path) {
|
|
1579
|
+
const match = this.router.match(method.toUpperCase(), path);
|
|
1580
|
+
if (!match.handler) {
|
|
1581
|
+
return this.handleNotFoundSync(request, path);
|
|
1582
|
+
}
|
|
1583
|
+
const cacheKey = `${method}:${match.routePattern ?? path}`;
|
|
1584
|
+
let entry = this.compiledDynamicRoutes.get(cacheKey);
|
|
1585
|
+
if (!entry || entry.version !== this.router.version) {
|
|
1586
|
+
const compiled = compileMiddlewareChain(match.middleware, match.handler);
|
|
1587
|
+
if (this.compiledDynamicRoutes.size > 1e3) {
|
|
1588
|
+
this.compiledDynamicRoutes.clear();
|
|
1589
|
+
}
|
|
1590
|
+
entry = { compiled, version: this.router.version };
|
|
1591
|
+
this.compiledDynamicRoutes.set(cacheKey, entry);
|
|
1592
|
+
}
|
|
1593
|
+
const ctx = this.contextPool.acquire();
|
|
1594
|
+
const execute = async () => {
|
|
1595
|
+
try {
|
|
1596
|
+
ctx.init(request, match.params, path, match.routePattern);
|
|
1597
|
+
return await entry?.compiled(ctx);
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
return await this.handleError(error, ctx);
|
|
1600
|
+
} finally {
|
|
1601
|
+
try {
|
|
1602
|
+
await ctx.requestScope().cleanup();
|
|
1603
|
+
} catch (cleanupError) {
|
|
1604
|
+
console.error("RequestScope cleanup failed:", cleanupError);
|
|
1605
|
+
}
|
|
1606
|
+
this.contextPool.release(ctx);
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
return execute();
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Sync error handler (for ultra-fast path)
|
|
1613
|
+
*/
|
|
1614
|
+
handleErrorSync(error, request, path) {
|
|
1615
|
+
if (this.errorHandler) {
|
|
1616
|
+
const ctx = new MinimalContext(request, {}, path);
|
|
1617
|
+
const result = this.errorHandler(error, ctx);
|
|
1618
|
+
if (result instanceof Response) {
|
|
1619
|
+
return result;
|
|
1620
|
+
}
|
|
1621
|
+
return result;
|
|
1622
|
+
}
|
|
1623
|
+
console.error("Unhandled error:", error);
|
|
1624
|
+
return new Response(CACHED_RESPONSES.INTERNAL_ERROR, {
|
|
1625
|
+
status: 500,
|
|
1626
|
+
headers: HEADERS.JSON
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Sync 404 handler (for ultra-fast path)
|
|
1631
|
+
*/
|
|
1632
|
+
handleNotFoundSync(request, path) {
|
|
1633
|
+
if (this.notFoundHandler) {
|
|
1634
|
+
const ctx = new MinimalContext(request, {}, path);
|
|
1635
|
+
const result = this.notFoundHandler(ctx);
|
|
1636
|
+
if (result instanceof Response) {
|
|
1637
|
+
return result;
|
|
1638
|
+
}
|
|
1639
|
+
return result;
|
|
1640
|
+
}
|
|
1641
|
+
return new Response(CACHED_RESPONSES.NOT_FOUND, {
|
|
1642
|
+
status: 404,
|
|
1643
|
+
headers: HEADERS.JSON
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Collect middleware for a specific path
|
|
1648
|
+
*/
|
|
1649
|
+
collectMiddlewareForPath(path, routeMiddleware) {
|
|
1650
|
+
if (this.router.globalMiddleware.length === 0 && this.router.pathMiddleware.size === 0) {
|
|
1651
|
+
return routeMiddleware;
|
|
1652
|
+
}
|
|
1653
|
+
return this.router.collectMiddlewarePublic(path, routeMiddleware);
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Compile routes for optimization
|
|
1657
|
+
*/
|
|
1658
|
+
compileRoutes() {
|
|
1659
|
+
this.staticRoutes = this.router.staticRoutes;
|
|
1660
|
+
const hasGlobalMiddleware = this.router.globalMiddleware.length > 0;
|
|
1661
|
+
const hasPathMiddleware = this.router.pathMiddleware.size > 0;
|
|
1662
|
+
this.isPureStaticApp = !hasGlobalMiddleware && !hasPathMiddleware;
|
|
1663
|
+
for (const [key, route] of this.staticRoutes) {
|
|
1664
|
+
if (route.compiledVersion === this.router.version) {
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
const analysis = analyzeHandler(route.handler);
|
|
1668
|
+
const optimalType = getOptimalContextType(analysis);
|
|
1669
|
+
route.useMinimal = this.isPureStaticApp && route.middleware.length === 0 && optimalType === "minimal";
|
|
1670
|
+
if (!route.useMinimal) {
|
|
1671
|
+
const allMiddleware = this.collectMiddlewareForPath(key.split(":")[1], route.middleware);
|
|
1672
|
+
route.compiled = compileMiddlewareChain(allMiddleware, route.handler);
|
|
1673
|
+
}
|
|
1674
|
+
route.compiledVersion = this.router.version;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Add a route to the router
|
|
1679
|
+
*/
|
|
1680
|
+
addRoute(method, path, handlers) {
|
|
1681
|
+
if (handlers.length === 0) {
|
|
1682
|
+
throw new Error(`No handler provided for ${method.toUpperCase()} ${path}`);
|
|
1683
|
+
}
|
|
1684
|
+
const handler = handlers[handlers.length - 1];
|
|
1685
|
+
const middleware = handlers.slice(0, -1);
|
|
1686
|
+
this.router.add(method, path, handler, middleware);
|
|
1687
|
+
this.compileRoutes();
|
|
1688
|
+
return this;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Execute middleware chain followed by handler
|
|
1692
|
+
*/
|
|
1693
|
+
async executeMiddleware(ctx, middleware, handler) {
|
|
1694
|
+
let index = 0;
|
|
1695
|
+
const next = async () => {
|
|
1696
|
+
if (index < middleware.length) {
|
|
1697
|
+
const mw = middleware[index++];
|
|
1698
|
+
return await mw(ctx, next);
|
|
1699
|
+
}
|
|
1700
|
+
return void 0;
|
|
1701
|
+
};
|
|
1702
|
+
const result = await next();
|
|
1703
|
+
if (result instanceof Response) {
|
|
1704
|
+
return result;
|
|
1705
|
+
}
|
|
1706
|
+
return await handler(ctx);
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Handle errors (Async version for dynamic/middleware paths)
|
|
1710
|
+
*/
|
|
1711
|
+
async handleError(error, ctx) {
|
|
1712
|
+
if (this.errorHandler) {
|
|
1713
|
+
return await this.errorHandler(error, ctx);
|
|
1714
|
+
}
|
|
1715
|
+
console.error("Unhandled error:", error);
|
|
1716
|
+
return ctx.json(
|
|
1717
|
+
{
|
|
1718
|
+
error: "Internal Server Error",
|
|
1719
|
+
message: error.message
|
|
1720
|
+
},
|
|
1721
|
+
500
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
export {
|
|
1726
|
+
AOTRouter,
|
|
1727
|
+
FastContext as FastContextImpl,
|
|
1728
|
+
Gravito,
|
|
1729
|
+
MinimalContext,
|
|
1730
|
+
ObjectPool,
|
|
1731
|
+
extractPath
|
|
1732
|
+
};
|