@nextrush/router 3.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.
- package/LICENSE +21 -0
- package/README.md +618 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +853 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/router-edge-cases.test.ts +486 -0
- package/src/__tests__/router.test.ts +621 -0
- package/src/index.ts +31 -0
- package/src/radix-tree.ts +184 -0
- package/src/router.ts +1029 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/router - Segment Trie Node
|
|
3
|
+
*
|
|
4
|
+
* Internal segment trie implementation for high-performance route matching.
|
|
5
|
+
* Uses a compressed trie structure for O(k) lookups where k is path length.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context, HttpMethod, Middleware, RouteHandler } from '@nextrush/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Node type enumeration
|
|
15
|
+
*/
|
|
16
|
+
export const enum NodeType {
|
|
17
|
+
/** Static path segment: /users */
|
|
18
|
+
STATIC = 0,
|
|
19
|
+
/** Named parameter: /:id */
|
|
20
|
+
PARAM = 1,
|
|
21
|
+
/** Wildcard: /* */
|
|
22
|
+
WILDCARD = 2,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Radix tree node
|
|
27
|
+
*/
|
|
28
|
+
export interface RadixNode {
|
|
29
|
+
/** Path segment for this node */
|
|
30
|
+
segment: string;
|
|
31
|
+
/** Node type */
|
|
32
|
+
type: NodeType;
|
|
33
|
+
/** Children nodes keyed by first character */
|
|
34
|
+
children: Map<string, RadixNode>;
|
|
35
|
+
/** Parameter name if this is a param node */
|
|
36
|
+
paramName?: string;
|
|
37
|
+
/** Handlers keyed by HTTP method */
|
|
38
|
+
handlers: Map<HttpMethod, HandlerEntry>;
|
|
39
|
+
/** Wildcard child if any */
|
|
40
|
+
wildcardChild?: RadixNode;
|
|
41
|
+
/** Parameter child if any */
|
|
42
|
+
paramChild?: RadixNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handler entry with middleware and pre-compiled executor
|
|
47
|
+
*/
|
|
48
|
+
export interface HandlerEntry {
|
|
49
|
+
handler: RouteHandler;
|
|
50
|
+
middleware: Middleware[];
|
|
51
|
+
/** Pre-compiled executor for fast dispatch (no closure per request) */
|
|
52
|
+
executor?: (ctx: Context) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* No-op next function - reusable, zero allocation
|
|
57
|
+
* Caches the resolved Promise to avoid per-call allocation
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
const RESOLVED_PROMISE = Promise.resolve();
|
|
61
|
+
export const NOOP_NEXT = (): Promise<void> => RESOLVED_PROMISE;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compile an executor for a route handler with middleware
|
|
65
|
+
* This creates the executor ONCE at registration time, not per-request
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
export function compileExecutor(
|
|
69
|
+
handler: RouteHandler,
|
|
70
|
+
middleware: Middleware[]
|
|
71
|
+
): (ctx: Context) => Promise<void> {
|
|
72
|
+
const len = middleware.length;
|
|
73
|
+
|
|
74
|
+
// FAST PATH: No middleware - direct handler call
|
|
75
|
+
if (len === 0) {
|
|
76
|
+
return async (ctx: Context) => {
|
|
77
|
+
await handler(ctx, NOOP_NEXT);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// FAST PATH: Single middleware
|
|
82
|
+
if (len === 1) {
|
|
83
|
+
const mw = middleware[0];
|
|
84
|
+
if (mw === undefined) throw new Error('middleware length mismatch');
|
|
85
|
+
return async (ctx: Context) => {
|
|
86
|
+
await mw(ctx, async () => {
|
|
87
|
+
await handler(ctx, NOOP_NEXT);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// FAST PATH: Two middleware (very common)
|
|
93
|
+
if (len === 2) {
|
|
94
|
+
const mw0 = middleware[0];
|
|
95
|
+
const mw1 = middleware[1];
|
|
96
|
+
if (mw0 === undefined || mw1 === undefined) throw new Error('middleware length mismatch');
|
|
97
|
+
return async (ctx: Context) => {
|
|
98
|
+
await mw0(ctx, async () => {
|
|
99
|
+
await mw1(ctx, async () => {
|
|
100
|
+
await handler(ctx, NOOP_NEXT);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// General case: Build recursive dispatch
|
|
107
|
+
// Note: This closure is created ONCE at registration, not per request
|
|
108
|
+
return async (ctx: Context): Promise<void> => {
|
|
109
|
+
let index = 0;
|
|
110
|
+
|
|
111
|
+
const dispatch = async (): Promise<void> => {
|
|
112
|
+
if (index < len) {
|
|
113
|
+
const mw = middleware[index++];
|
|
114
|
+
if (mw === undefined) throw new Error('middleware length mismatch');
|
|
115
|
+
await mw(ctx, dispatch);
|
|
116
|
+
} else {
|
|
117
|
+
await handler(ctx, NOOP_NEXT);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await dispatch();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create a new radix node
|
|
127
|
+
*/
|
|
128
|
+
export function createNode(segment: string, type: NodeType = NodeType.STATIC): RadixNode {
|
|
129
|
+
return {
|
|
130
|
+
segment,
|
|
131
|
+
type,
|
|
132
|
+
children: new Map(),
|
|
133
|
+
handlers: new Map(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse path segments
|
|
139
|
+
* Splits path into segments and identifies param/wildcard types
|
|
140
|
+
*
|
|
141
|
+
* @param path - Route path to parse
|
|
142
|
+
* @param caseSensitive - If false, lowercase static segments for case-insensitive matching
|
|
143
|
+
*/
|
|
144
|
+
export function parseSegments(path: string, caseSensitive = true): ParsedSegment[] {
|
|
145
|
+
const normalized = path.startsWith('/') ? path.slice(1) : path;
|
|
146
|
+
if (normalized === '') return [];
|
|
147
|
+
|
|
148
|
+
const parts = normalized.split('/');
|
|
149
|
+
const segments: ParsedSegment[] = [];
|
|
150
|
+
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
if (part.startsWith(':')) {
|
|
153
|
+
// Preserve the original parameter name case
|
|
154
|
+
const paramName = part.slice(1);
|
|
155
|
+
segments.push({
|
|
156
|
+
segment: part, // Preserve original case — param nodes match any segment
|
|
157
|
+
type: NodeType.PARAM,
|
|
158
|
+
paramName, // Keep original case
|
|
159
|
+
});
|
|
160
|
+
} else if (part === '*') {
|
|
161
|
+
segments.push({
|
|
162
|
+
segment: '*',
|
|
163
|
+
type: NodeType.WILDCARD,
|
|
164
|
+
});
|
|
165
|
+
break; // Wildcard must be last
|
|
166
|
+
} else {
|
|
167
|
+
segments.push({
|
|
168
|
+
segment: caseSensitive ? part : part.toLowerCase(),
|
|
169
|
+
type: NodeType.STATIC,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return segments;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Parsed segment structure
|
|
179
|
+
*/
|
|
180
|
+
export interface ParsedSegment {
|
|
181
|
+
segment: string;
|
|
182
|
+
type: NodeType;
|
|
183
|
+
paramName?: string;
|
|
184
|
+
}
|