@esportsplus/routing 0.1.32 → 0.2.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/bench/index.mjs +217 -0
- package/build/browser.js +2 -2
- package/build/constants.d.ts +2 -2
- package/build/constants.js +2 -2
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -1
- package/build/router/index.d.ts +5 -3
- package/build/router/index.js +45 -42
- package/build/router/node.d.ts +4 -3
- package/build/router/node.js +41 -54
- package/package.json +5 -3
- package/src/browser.ts +3 -2
- package/src/constants.ts +2 -2
- package/src/index.ts +0 -1
- package/src/router/index.ts +55 -47
- package/src/router/node.ts +52 -71
- package/src/types.ts +6 -1
- package/build/slugify.d.ts +0 -2
- package/build/slugify.js +0 -3
- package/src/slugify.ts +0 -4
package/bench/index.mjs
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import routerFactory from '../build/router/index.js';
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return new Intl.NumberFormat('en-US').format(n);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function timeit(fn, durationMs = 1000) {
|
|
9
|
+
// Warmup
|
|
10
|
+
for (let i = 0; i < 5_000; i++) fn();
|
|
11
|
+
|
|
12
|
+
const end = performance.now() + durationMs;
|
|
13
|
+
let count = 0;
|
|
14
|
+
while (performance.now() < end) {
|
|
15
|
+
fn();
|
|
16
|
+
count++;
|
|
17
|
+
}
|
|
18
|
+
return count * (1000 / durationMs);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildStaticPaths(length, count) {
|
|
22
|
+
const paths = new Array(count);
|
|
23
|
+
for (let i = 0; i < count; i++) {
|
|
24
|
+
const id = i.toString(36).padStart(3, '0');
|
|
25
|
+
paths[i] = '/a'.repeat(length) + '/' + id;
|
|
26
|
+
}
|
|
27
|
+
return paths;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildDynamicPaths(length, count) {
|
|
31
|
+
// Route patterns
|
|
32
|
+
const patterns = new Array(count);
|
|
33
|
+
for (let i = 0; i < count; i++) {
|
|
34
|
+
patterns[i] = '/a'.repeat(length) + '/:id';
|
|
35
|
+
}
|
|
36
|
+
return patterns;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildDynamicTestPaths(length, count) {
|
|
40
|
+
// Concrete paths to match against dynamic patterns
|
|
41
|
+
const paths = new Array(count);
|
|
42
|
+
for (let i = 0; i < count; i++) {
|
|
43
|
+
const id = i.toString(36).padStart(3, '0');
|
|
44
|
+
paths[i] = '/a'.repeat(length) + '/' + id;
|
|
45
|
+
}
|
|
46
|
+
return paths;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildWildcardPatterns(length, count) {
|
|
50
|
+
// Unique patterns: /a.../<token>/*:rest
|
|
51
|
+
const patterns = new Array(count);
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const token = i.toString(36);
|
|
54
|
+
patterns[i] = '/a'.repeat(length) + '/' + token + '/*:rest';
|
|
55
|
+
}
|
|
56
|
+
return patterns;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildWildcardTestPaths(length, count, tailSegments = 4) {
|
|
60
|
+
// Concrete paths that should match the wildcard patterns above
|
|
61
|
+
const paths = new Array(count);
|
|
62
|
+
const tail = Array.from({ length: tailSegments }, (_, j) => 'x' + j).join('/');
|
|
63
|
+
for (let i = 0; i < count; i++) {
|
|
64
|
+
const token = i.toString(36);
|
|
65
|
+
paths[i] = '/a'.repeat(length) + '/' + token + '/' + tail;
|
|
66
|
+
}
|
|
67
|
+
return paths;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildOptionalPatterns(baseLen, optionalCount, count) {
|
|
71
|
+
// Unique patterns: /a.../<token>?:p1?:p2?...?:pN
|
|
72
|
+
const patterns = new Array(count);
|
|
73
|
+
for (let i = 0; i < count; i++) {
|
|
74
|
+
const token = i.toString(36);
|
|
75
|
+
let p = '/a'.repeat(baseLen) + '/' + token;
|
|
76
|
+
for (let k = 1; k <= optionalCount; k++) {
|
|
77
|
+
p += '?:p' + k;
|
|
78
|
+
}
|
|
79
|
+
patterns[i] = p;
|
|
80
|
+
}
|
|
81
|
+
return patterns;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildOptionalTestPaths(baseLen, optionalCount, count) {
|
|
85
|
+
// Concrete paths using the max number of optionals to exercise the longest route
|
|
86
|
+
const paths = new Array(count);
|
|
87
|
+
for (let i = 0; i < count; i++) {
|
|
88
|
+
const token = i.toString(36);
|
|
89
|
+
let p = '/a'.repeat(baseLen) + '/' + token;
|
|
90
|
+
for (let k = 1; k <= optionalCount; k++) {
|
|
91
|
+
p += '/' + 'v' + k;
|
|
92
|
+
}
|
|
93
|
+
paths[i] = p;
|
|
94
|
+
}
|
|
95
|
+
return paths;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function benchAdd(paths, { dynamic = false, resetEvery = 25000, uniquePath } = {}) {
|
|
99
|
+
let router = routerFactory();
|
|
100
|
+
const resp = (req) => 1;
|
|
101
|
+
let k = 0;
|
|
102
|
+
let unique = 0;
|
|
103
|
+
|
|
104
|
+
return timeit(() => {
|
|
105
|
+
// Generate a unique path to avoid duplicate key errors
|
|
106
|
+
const base = paths[k++ % paths.length];
|
|
107
|
+
const path = uniquePath
|
|
108
|
+
? uniquePath(base, unique++)
|
|
109
|
+
: (dynamic ? (base + '/x' + (unique++)) : (base + '-' + (unique++)));
|
|
110
|
+
router.get({ path, responder: resp });
|
|
111
|
+
|
|
112
|
+
// Periodically reset to keep memory bounded
|
|
113
|
+
if (unique % resetEvery === 0) {
|
|
114
|
+
router = routerFactory();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function benchMatch(router, paths, { method = 'GET' } = {}) {
|
|
120
|
+
let k = 0;
|
|
121
|
+
return timeit(() => {
|
|
122
|
+
const path = paths[k++ % paths.length];
|
|
123
|
+
const { route } = router.match(method, path);
|
|
124
|
+
if (!route) throw new Error('route not found');
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function prepareRouterStatic(paths) {
|
|
129
|
+
const router = routerFactory();
|
|
130
|
+
const resp = (req) => 1;
|
|
131
|
+
for (const p of paths) router.get({ path: p, responder: resp });
|
|
132
|
+
return router;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function prepareRouterDynamic(paths) {
|
|
136
|
+
const router = routerFactory();
|
|
137
|
+
const resp = (req) => 1;
|
|
138
|
+
for (const p of paths) router.get({ path: p, responder: resp });
|
|
139
|
+
return router;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function section(title) {
|
|
143
|
+
console.log('\n' + title);
|
|
144
|
+
console.log('-'.repeat(title.length));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
(async function main() {
|
|
148
|
+
const lengths = [1, 2, 4, 8];
|
|
149
|
+
const poolSize = 1_000;
|
|
150
|
+
|
|
151
|
+
section('Add routes (ops/sec)');
|
|
152
|
+
for (const len of lengths) {
|
|
153
|
+
const staticPaths = buildStaticPaths(len, poolSize);
|
|
154
|
+
const dynamicPatterns = buildDynamicPaths(len, poolSize);
|
|
155
|
+
|
|
156
|
+
const addS = benchAdd(staticPaths);
|
|
157
|
+
const addD = benchAdd(dynamicPatterns, { dynamic: true });
|
|
158
|
+
|
|
159
|
+
console.log(`length=${len} static_add=${fmt(addS)} dynamic_add=${fmt(addD)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
section('Match routes (ops/sec)');
|
|
163
|
+
for (const len of lengths) {
|
|
164
|
+
const staticPaths = buildStaticPaths(len, poolSize);
|
|
165
|
+
const dynamicPatterns = buildDynamicPaths(len, poolSize);
|
|
166
|
+
const dynamicTestPaths = buildDynamicTestPaths(len, poolSize);
|
|
167
|
+
|
|
168
|
+
const routerS = prepareRouterStatic(staticPaths);
|
|
169
|
+
const routerD = prepareRouterDynamic(dynamicPatterns);
|
|
170
|
+
|
|
171
|
+
const matchS = benchMatch(routerS, staticPaths);
|
|
172
|
+
const matchD = benchMatch(routerD, dynamicTestPaths);
|
|
173
|
+
|
|
174
|
+
console.log(`length=${len} static_match=${fmt(matchS)} dynamic_match=${fmt(matchD)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Wildcard creation and matching
|
|
178
|
+
section('Wildcard routes (ops/sec)');
|
|
179
|
+
for (const len of lengths) {
|
|
180
|
+
const wildcardPatterns = buildWildcardPatterns(len, poolSize);
|
|
181
|
+
// For wildcard, uniqueness must be inserted before '/*:rest'
|
|
182
|
+
const addW = benchAdd(wildcardPatterns, {
|
|
183
|
+
dynamic: true,
|
|
184
|
+
uniquePath: (base, n) => {
|
|
185
|
+
const idx = base.indexOf('/*:rest');
|
|
186
|
+
return base.slice(0, idx) + '-u' + n + base.slice(idx);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const routerW = prepareRouterDynamic(wildcardPatterns);
|
|
191
|
+
const wildcardTests = buildWildcardTestPaths(len, poolSize, 8);
|
|
192
|
+
const matchW = benchMatch(routerW, wildcardTests);
|
|
193
|
+
|
|
194
|
+
console.log(`length=${len} wildcard_add=${fmt(addW)} wildcard_match=${fmt(matchW)}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Optional parameter count scaling
|
|
198
|
+
section('Optional params scaling (ops/sec)');
|
|
199
|
+
const optionalCounts = [1, 2, 4, 8];
|
|
200
|
+
const baseLen = 2;
|
|
201
|
+
for (const nOpt of optionalCounts) {
|
|
202
|
+
const optPatterns = buildOptionalPatterns(baseLen, nOpt, poolSize);
|
|
203
|
+
const addO = benchAdd(optPatterns, {
|
|
204
|
+
dynamic: true,
|
|
205
|
+
uniquePath: (base, n) => {
|
|
206
|
+
const idx = base.indexOf('?:');
|
|
207
|
+
return idx === -1 ? (base + '-u' + n) : (base.slice(0, idx) + '-u' + n + base.slice(idx));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const routerO = prepareRouterDynamic(optPatterns);
|
|
212
|
+
const optTests = buildOptionalTestPaths(baseLen, nOpt, poolSize);
|
|
213
|
+
const matchO = benchMatch(routerO, optTests);
|
|
214
|
+
|
|
215
|
+
console.log(`optionals=${nOpt} opt_add=${fmt(addO)} opt_match=${fmt(matchO)}`);
|
|
216
|
+
}
|
|
217
|
+
})();
|
package/build/browser.js
CHANGED
|
@@ -29,9 +29,9 @@ function href() {
|
|
|
29
29
|
}
|
|
30
30
|
function match(request, router, subdomain) {
|
|
31
31
|
if (router.subdomains !== null) {
|
|
32
|
-
let subdomains = router.subdomains;
|
|
32
|
+
let hostname = request.hostname, subdomains = router.subdomains;
|
|
33
33
|
for (let i = 0, n = subdomains.length; i < n; i++) {
|
|
34
|
-
if (!
|
|
34
|
+
if (!hostname.startsWith(subdomains[i])) {
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
37
|
subdomain = subdomains[i];
|
package/build/constants.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ declare const ON_DELETE: string[];
|
|
|
2
2
|
declare const ON_GET: string[];
|
|
3
3
|
declare const ON_POST: string[];
|
|
4
4
|
declare const ON_PUT: string[];
|
|
5
|
-
declare const
|
|
5
|
+
declare const PARAMETER = 0;
|
|
6
6
|
declare const STATIC = 1;
|
|
7
7
|
declare const WILDCARD = 2;
|
|
8
|
-
export { ON_DELETE, ON_GET, ON_POST, ON_PUT,
|
|
8
|
+
export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
|
package/build/constants.js
CHANGED
|
@@ -2,7 +2,7 @@ const ON_DELETE = ['DELETE'];
|
|
|
2
2
|
const ON_GET = ['GET'];
|
|
3
3
|
const ON_POST = ['POST'];
|
|
4
4
|
const ON_PUT = ['PUT'];
|
|
5
|
-
const
|
|
5
|
+
const PARAMETER = 0;
|
|
6
6
|
const STATIC = 1;
|
|
7
7
|
const WILDCARD = 2;
|
|
8
|
-
export { ON_DELETE, ON_GET, ON_POST, ON_PUT,
|
|
8
|
+
export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
|
package/build/index.d.ts
CHANGED
package/build/index.js
CHANGED
package/build/router/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Name, Options, Route, RouteOptions } from '../types.js';
|
|
2
2
|
import { Node } from './node.js';
|
|
3
|
+
declare function key(method: string, subdomain?: string | null): string;
|
|
3
4
|
declare class Router<T> {
|
|
5
|
+
bucket: Record<ReturnType<typeof key>, {
|
|
6
|
+
root: Node<T>;
|
|
7
|
+
static: Record<string, Route<T>>;
|
|
8
|
+
}>;
|
|
4
9
|
groups: Options<T>[];
|
|
5
|
-
root: Node<T>;
|
|
6
10
|
routes: Record<Name, Route<T>>;
|
|
7
|
-
static: Record<Name, Route<T>>;
|
|
8
11
|
subdomains: string[] | null;
|
|
9
|
-
constructor();
|
|
10
12
|
private add;
|
|
11
13
|
private route;
|
|
12
14
|
delete(options: RouteOptions<T>): this;
|
package/build/router/index.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
import { ON_DELETE, ON_GET, ON_POST, ON_PUT
|
|
1
|
+
import { ON_DELETE, ON_GET, ON_POST, ON_PUT } from '../constants.js';
|
|
2
2
|
import { Node } from './node.js';
|
|
3
3
|
import pipeline from '@esportsplus/pipeline';
|
|
4
|
+
function key(method, subdomain) {
|
|
5
|
+
return (method + (subdomain ? ' ' + subdomain : '')).toUpperCase();
|
|
6
|
+
}
|
|
4
7
|
function normalize(path) {
|
|
5
8
|
if (path) {
|
|
6
9
|
if (path[0] !== '/') {
|
|
7
10
|
path = '/' + path;
|
|
8
11
|
}
|
|
9
|
-
if (path.
|
|
12
|
+
if (path.length > 1 && path[path.length - 1] === '/') {
|
|
10
13
|
path = path.slice(0, -1);
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
return path || '/';
|
|
14
17
|
}
|
|
15
|
-
function radixkey(method, path, subdomain) {
|
|
16
|
-
return ((subdomain ? subdomain + ' ' : '') + method).toUpperCase() + ' ' + normalize(path);
|
|
17
|
-
}
|
|
18
18
|
function set(route, options) {
|
|
19
|
+
let pipeline = route.pipeline;
|
|
19
20
|
for (let key in options) {
|
|
20
21
|
let value = options[key];
|
|
21
22
|
if (key === 'middleware') {
|
|
22
23
|
for (let i = 0, n = value.length; i < n; i++) {
|
|
23
|
-
|
|
24
|
+
pipeline.add(value[i]);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
else if (key === 'responder') {
|
|
27
|
-
|
|
28
|
+
pipeline.add(value);
|
|
28
29
|
}
|
|
29
30
|
else {
|
|
30
31
|
route[key] = (route[key] || '') + value;
|
|
@@ -32,20 +33,23 @@ function set(route, options) {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
class Router {
|
|
36
|
+
bucket = {};
|
|
35
37
|
groups = [];
|
|
36
|
-
root;
|
|
37
38
|
routes = {};
|
|
38
|
-
static = {};
|
|
39
39
|
subdomains = null;
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
add(method, path, route) {
|
|
41
|
+
let bucket = this.bucket[key(method, route.subdomain)] ??= {
|
|
42
|
+
root: new Node(),
|
|
43
|
+
static: {}
|
|
44
|
+
};
|
|
45
|
+
if (path.indexOf(':') === -1) {
|
|
46
|
+
if (path in bucket.static) {
|
|
47
|
+
throw new Error(`Routing: static path '${path}' is already in use`);
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
bucket.static[path] = route;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
bucket.root.add(path, route);
|
|
49
53
|
}
|
|
50
54
|
return this;
|
|
51
55
|
}
|
|
@@ -84,44 +88,43 @@ class Router {
|
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
match(method, path, subdomain) {
|
|
87
|
-
let
|
|
88
|
-
if (
|
|
89
|
-
return {
|
|
90
|
-
route: this.static[key]
|
|
91
|
-
};
|
|
91
|
+
let bucket = this.bucket[key(method, subdomain)];
|
|
92
|
+
if (!bucket) {
|
|
93
|
+
return {};
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
path = normalize(path);
|
|
96
|
+
if (path in bucket.static) {
|
|
97
|
+
return { route: bucket.static[path] };
|
|
98
|
+
}
|
|
99
|
+
return bucket.root.find(path);
|
|
94
100
|
}
|
|
95
101
|
on(methods, options) {
|
|
96
102
|
let route = this.route(options);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
let name = route.name, path = route.path, subdomain = route.subdomain;
|
|
104
|
+
if (name) {
|
|
105
|
+
if (this.routes[name]) {
|
|
106
|
+
throw new Error(`Routing: '${name}' is already in use`);
|
|
100
107
|
}
|
|
101
|
-
this.routes[
|
|
108
|
+
this.routes[name] = route;
|
|
102
109
|
}
|
|
103
|
-
if (
|
|
110
|
+
if (path) {
|
|
104
111
|
for (let i = 0, n = methods.length; i < n; i++) {
|
|
105
|
-
let
|
|
106
|
-
if (
|
|
107
|
-
let segments =
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
let method = methods[i];
|
|
113
|
+
if (path.indexOf('?:') !== -1) {
|
|
114
|
+
let segments = path.split('?:'), url = segments[0];
|
|
115
|
+
this.add(method, url, route);
|
|
116
|
+
for (let i = 1; i < segments.length; i++) {
|
|
117
|
+
url += '/:' + segments[i];
|
|
118
|
+
this.add(method, url, route);
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
121
|
else {
|
|
113
|
-
this.add(
|
|
122
|
+
this.add(method, path, route);
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
if (!this.subdomains) {
|
|
120
|
-
this.subdomains = [subdomain];
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
this.subdomains.push(subdomain);
|
|
124
|
-
}
|
|
126
|
+
if (subdomain) {
|
|
127
|
+
(this.subdomains ??= []).push(subdomain.toLowerCase());
|
|
125
128
|
}
|
|
126
129
|
return this;
|
|
127
130
|
}
|
package/build/router/node.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Route } from './index.js';
|
|
2
2
|
declare class Node<T> {
|
|
3
|
-
children: Map<string | number, Node<T>> | null;
|
|
4
3
|
parent: Node<T> | null;
|
|
5
4
|
path: string | null;
|
|
6
|
-
property: string | null;
|
|
7
5
|
route: Route<T> | null;
|
|
6
|
+
static: Map<string | number, Node<T>> | null;
|
|
8
7
|
type: number | null;
|
|
8
|
+
name: string | null;
|
|
9
|
+
parameter: Node<T> | null;
|
|
10
|
+
wildcard: Node<T> | null;
|
|
9
11
|
constructor(parent?: Node<T>['parent']);
|
|
10
12
|
add(path: string, route: Route<T>): Node<T>;
|
|
11
13
|
find(path: string): {
|
|
12
14
|
parameters?: Readonly<Record<PropertyKey, unknown>>;
|
|
13
15
|
route?: Readonly<Route<T>>;
|
|
14
16
|
};
|
|
15
|
-
remove(path: string): void;
|
|
16
17
|
}
|
|
17
18
|
export { Node };
|
package/build/router/node.js
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PARAMETER, STATIC, WILDCARD } from '../constants.js';
|
|
2
2
|
class Node {
|
|
3
|
-
children = null;
|
|
4
3
|
parent = null;
|
|
5
4
|
path = null;
|
|
6
|
-
property = null;
|
|
7
5
|
route = null;
|
|
6
|
+
static = null;
|
|
8
7
|
type = null;
|
|
8
|
+
name = null;
|
|
9
|
+
parameter = null;
|
|
10
|
+
wildcard = null;
|
|
9
11
|
constructor(parent = null) {
|
|
10
12
|
this.parent = parent;
|
|
11
13
|
}
|
|
12
14
|
add(path, route) {
|
|
13
15
|
let node = this, segments = path.split('/'), type = STATIC, unnamed = 0;
|
|
14
16
|
for (let i = 0, n = segments.length; i < n; i++) {
|
|
15
|
-
let
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
node.
|
|
17
|
+
let segment = segments[i], symbol = segment[0];
|
|
18
|
+
if (symbol === ':') {
|
|
19
|
+
if (!node.parameter) {
|
|
20
|
+
node.parameter = new Node(node);
|
|
21
|
+
node.parameter.name = (segment.slice(1) || unnamed++).toString();
|
|
20
22
|
}
|
|
21
|
-
node
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
node = node.parameter;
|
|
24
|
+
type = PARAMETER;
|
|
25
|
+
}
|
|
26
|
+
else if (symbol === '*') {
|
|
27
|
+
if (!node.wildcard) {
|
|
28
|
+
node.wildcard = new Node(node);
|
|
29
|
+
node.wildcard.name = (segment.slice(2) || unnamed++).toString();
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
node = node.wildcard;
|
|
32
|
+
type = WILDCARD;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
let next = node.static?.get(segment);
|
|
36
|
+
if (!next) {
|
|
37
|
+
next = new Node(node);
|
|
38
|
+
(node.static ??= new Map()).set(segment, next);
|
|
31
39
|
}
|
|
40
|
+
node = next;
|
|
32
41
|
}
|
|
33
|
-
node = child;
|
|
34
42
|
}
|
|
35
43
|
node.path = path;
|
|
36
44
|
node.route = route;
|
|
@@ -38,56 +46,35 @@ class Node {
|
|
|
38
46
|
return node;
|
|
39
47
|
}
|
|
40
48
|
find(path) {
|
|
41
|
-
let node = this, parameters
|
|
49
|
+
let node = this, parameters, segments = path.split('/'), wildcard;
|
|
42
50
|
for (let i = 0, n = segments.length; i < n; i++) {
|
|
43
|
-
let segment = segments[i]
|
|
44
|
-
if (
|
|
51
|
+
let segment = segments[i];
|
|
52
|
+
if (node.wildcard) {
|
|
45
53
|
wildcard = {
|
|
46
|
-
node:
|
|
47
|
-
|
|
54
|
+
node: node.wildcard,
|
|
55
|
+
start: i
|
|
48
56
|
};
|
|
49
57
|
}
|
|
50
|
-
let next = node.
|
|
58
|
+
let next = node.static?.get(segment);
|
|
51
59
|
if (next) {
|
|
52
60
|
node = next;
|
|
61
|
+
continue;
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
node =
|
|
56
|
-
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
parameters[node.property] = segment;
|
|
63
|
+
if (!node.parameter) {
|
|
64
|
+
node = undefined;
|
|
65
|
+
break;
|
|
60
66
|
}
|
|
67
|
+
node = node.parameter;
|
|
68
|
+
(parameters ??= {})[node.name] = segment;
|
|
61
69
|
}
|
|
62
|
-
if ((node === undefined || node.route === null) && wildcard
|
|
70
|
+
if ((node === undefined || node.route === null) && wildcard) {
|
|
63
71
|
node = wildcard.node;
|
|
64
|
-
parameters[node.
|
|
65
|
-
}
|
|
66
|
-
if (!node) {
|
|
67
|
-
return {};
|
|
72
|
+
(parameters ??= {})[node.name] = segments.slice(wildcard.start).join('/');
|
|
68
73
|
}
|
|
69
74
|
return {
|
|
70
75
|
parameters,
|
|
71
|
-
route: node
|
|
76
|
+
route: node?.route || undefined
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
|
-
remove(path) {
|
|
75
|
-
let node = this, segments = path.split('/');
|
|
76
|
-
for (let i = 0, n = segments.length; i < n; i++) {
|
|
77
|
-
node = node.children?.get(segments[i]);
|
|
78
|
-
if (!node) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (node.children?.size) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
let parent = node.parent;
|
|
86
|
-
if (parent && parent.children) {
|
|
87
|
-
parent.children.delete(segments[segments.length - 1]);
|
|
88
|
-
parent.children.delete(WILDCARD);
|
|
89
|
-
parent.children.delete(PLACEHOLDER);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
79
|
}
|
|
93
80
|
export { Node };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@esportsplus/pipeline": "^1.1.5",
|
|
5
5
|
"@esportsplus/reactivity": "^0.12.4",
|
|
6
|
-
"@esportsplus/utilities": "^0.
|
|
6
|
+
"@esportsplus/utilities": "^0.22.1"
|
|
7
7
|
},
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"@esportsplus/typescript": "^0.9.2"
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
"name": "@esportsplus/routing",
|
|
13
13
|
"private": false,
|
|
14
14
|
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
15
16
|
"types": "./build/index.d.ts",
|
|
16
|
-
"version": "0.1
|
|
17
|
+
"version": "0.2.1",
|
|
17
18
|
"scripts": {
|
|
18
19
|
"build": "tsc && tsc-alias",
|
|
19
|
-
"-": "-"
|
|
20
|
+
"-": "-",
|
|
21
|
+
"bench": "node bench/index.mjs"
|
|
20
22
|
}
|
|
21
23
|
}
|
package/src/browser.ts
CHANGED
|
@@ -44,10 +44,11 @@ function href<T>() {
|
|
|
44
44
|
|
|
45
45
|
function match<T>(request: Request<T>, router: Router<T>, subdomain?: string) {
|
|
46
46
|
if (router.subdomains !== null) {
|
|
47
|
-
let
|
|
47
|
+
let hostname = request.hostname,
|
|
48
|
+
subdomains = router.subdomains;
|
|
48
49
|
|
|
49
50
|
for (let i = 0, n = subdomains.length; i < n; i++) {
|
|
50
|
-
if (!
|
|
51
|
+
if (!hostname.startsWith(subdomains[i])) {
|
|
51
52
|
continue;
|
|
52
53
|
}
|
|
53
54
|
|
package/src/constants.ts
CHANGED
|
@@ -7,11 +7,11 @@ const ON_POST = ['POST'];
|
|
|
7
7
|
const ON_PUT = ['PUT'];
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const PARAMETER = 0;
|
|
11
11
|
|
|
12
12
|
const STATIC = 1;
|
|
13
13
|
|
|
14
14
|
const WILDCARD = 2;
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
export { ON_DELETE, ON_GET, ON_POST, ON_PUT,
|
|
17
|
+
export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
|
package/src/index.ts
CHANGED
package/src/router/index.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { ON_DELETE, ON_GET, ON_POST, ON_PUT
|
|
2
|
-
import { Name, Options, Request, Route, RouteOptions } from '
|
|
1
|
+
import { ON_DELETE, ON_GET, ON_POST, ON_PUT } from '../constants';
|
|
2
|
+
import { Name, Options, Request, Route, RouteOptions } from '../types';
|
|
3
3
|
import { Node } from './node';
|
|
4
4
|
import pipeline from '@esportsplus/pipeline';
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
function key(method: string, subdomain?: string | null) {
|
|
8
|
+
return (method + (subdomain ? ' ' + subdomain : '')).toUpperCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
function normalize(path: string) {
|
|
8
12
|
if (path) {
|
|
9
13
|
if (path[0] !== '/') {
|
|
10
14
|
path = '/' + path;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
if (path.
|
|
17
|
+
if (path.length > 1 && path[path.length - 1] === '/') {
|
|
14
18
|
path = path.slice(0, -1);
|
|
15
19
|
}
|
|
16
20
|
}
|
|
@@ -18,21 +22,19 @@ function normalize(path: string) {
|
|
|
18
22
|
return path || '/';
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
function radixkey(method: string, path: string, subdomain?: string | null) {
|
|
22
|
-
return ((subdomain ? subdomain + ' ' : '') + method).toUpperCase() + ' ' + normalize(path);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
25
|
function set<T>(route: Route<T>, options: Options<T> | RouteOptions<T>) {
|
|
26
|
+
let pipeline = route.pipeline;
|
|
27
|
+
|
|
26
28
|
for (let key in options) {
|
|
27
29
|
let value = options[key as keyof typeof options] as any;
|
|
28
30
|
|
|
29
31
|
if (key === 'middleware') {
|
|
30
32
|
for (let i = 0, n = value.length; i < n; i++) {
|
|
31
|
-
|
|
33
|
+
pipeline.add(value[i]);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
else if (key === 'responder') {
|
|
35
|
-
|
|
37
|
+
pipeline.add(value);
|
|
36
38
|
}
|
|
37
39
|
else {
|
|
38
40
|
// @ts-ignore
|
|
@@ -43,25 +45,27 @@ function set<T>(route: Route<T>, options: Options<T> | RouteOptions<T>) {
|
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
class Router<T> {
|
|
48
|
+
bucket: Record<ReturnType<typeof key>, { root: Node<T>, static: Record<string, Route<T>> }> = {};
|
|
46
49
|
groups: Options<T>[] = [];
|
|
47
|
-
root: Node<T>;
|
|
48
50
|
routes: Record<Name, Route<T>> = {};
|
|
49
|
-
static: Record<Name, Route<T>> = {};
|
|
50
51
|
subdomains: string[] | null = null;
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
private add(method: string, path: string, route: Route<T>) {
|
|
55
|
+
let bucket = this.bucket[ key(method, route.subdomain) ] ??= {
|
|
56
|
+
root: new Node(),
|
|
57
|
+
static: {}
|
|
58
|
+
};
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw new Error(`Routing: static path '${radixkey}' is already in use`);
|
|
60
|
+
if (path.indexOf(':') === -1) {
|
|
61
|
+
if (path in bucket.static) {
|
|
62
|
+
throw new Error(`Routing: static path '${path}' is already in use`);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
bucket.static[path] = route;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
bucket.root.add(path, route);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
return this;
|
|
@@ -113,55 +117,59 @@ class Router<T> {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
match(method: string, path: string, subdomain?: string | null) {
|
|
116
|
-
let
|
|
120
|
+
let bucket = this.bucket[ key(method, subdomain) ];
|
|
117
121
|
|
|
118
|
-
if (
|
|
119
|
-
return {
|
|
120
|
-
route: this.static[key] as Readonly<Route<T>>
|
|
121
|
-
};
|
|
122
|
+
if (!bucket) {
|
|
123
|
+
return {};
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
path = normalize(path);
|
|
127
|
+
|
|
128
|
+
if (path in bucket.static) {
|
|
129
|
+
return { route: bucket.static[path] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return bucket.root.find(path);
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
on(methods: string[], options: RouteOptions<T>) {
|
|
128
136
|
let route = this.route(options);
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
let name = route.name,
|
|
139
|
+
path = route.path,
|
|
140
|
+
subdomain = route.subdomain;
|
|
141
|
+
|
|
142
|
+
if (name) {
|
|
143
|
+
if (this.routes[name]) {
|
|
144
|
+
throw new Error(`Routing: '${name}' is already in use`);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
this.routes[
|
|
147
|
+
this.routes[name] = route;
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
if (
|
|
150
|
+
if (path) {
|
|
139
151
|
for (let i = 0, n = methods.length; i < n; i++) {
|
|
140
|
-
let
|
|
152
|
+
let method = methods[i];
|
|
141
153
|
|
|
142
|
-
if (
|
|
143
|
-
let segments =
|
|
144
|
-
url =
|
|
154
|
+
if (path.indexOf('?:') !== -1) {
|
|
155
|
+
let segments = path.split('?:'),
|
|
156
|
+
url = segments[0];
|
|
145
157
|
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
this.add(method, url, route);
|
|
159
|
+
|
|
160
|
+
for (let i = 1; i < segments.length; i++) {
|
|
161
|
+
url += '/:' + segments[i];
|
|
162
|
+
this.add(method, url, route);
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
else {
|
|
151
|
-
this.add(
|
|
166
|
+
this.add(method, path, route);
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!this.subdomains) {
|
|
160
|
-
this.subdomains = [subdomain];
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
this.subdomains.push(subdomain);
|
|
164
|
-
}
|
|
171
|
+
if (subdomain) {
|
|
172
|
+
(this.subdomains ??= []).push( subdomain.toLowerCase() );
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
return this;
|
package/src/router/node.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PARAMETER, STATIC, WILDCARD } from '../constants';
|
|
2
2
|
import { Route } from './index';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Node<T> {
|
|
6
|
-
children: Map<string | number, Node<T>> | null = null;
|
|
7
6
|
parent: Node<T> | null = null;
|
|
8
7
|
path: string | null = null;
|
|
9
|
-
property: string | null = null;
|
|
10
8
|
route: Route<T> | null = null;
|
|
9
|
+
static: Map<string | number, Node<T>> | null = null;
|
|
11
10
|
type: number | null = null;
|
|
12
11
|
|
|
12
|
+
// Parameter or Wildcard parameter name
|
|
13
|
+
name: string | null = null;
|
|
14
|
+
parameter: Node<T> | null = null;
|
|
15
|
+
wildcard: Node<T> | null = null;
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
constructor(parent: Node<T>['parent'] = null) {
|
|
15
19
|
this.parent = parent;
|
|
@@ -23,33 +27,40 @@ class Node<T> {
|
|
|
23
27
|
unnamed = 0;
|
|
24
28
|
|
|
25
29
|
for (let i = 0, n = segments.length; i < n; i++) {
|
|
26
|
-
let
|
|
30
|
+
let segment = segments[i],
|
|
31
|
+
symbol = segment[0];
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
// Parameter
|
|
34
|
+
if (symbol === ':') {
|
|
35
|
+
if (!node.parameter) {
|
|
36
|
+
node.parameter = new Node<T>(node);
|
|
37
|
+
node.parameter.name = (segment.slice(1) || unnamed++).toString();
|
|
38
|
+
}
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
node = node.parameter;
|
|
41
|
+
type = PARAMETER;
|
|
42
|
+
}
|
|
43
|
+
// "*:" Wildcard
|
|
44
|
+
else if (symbol === '*') {
|
|
45
|
+
if (!node.wildcard) {
|
|
46
|
+
node.wildcard = new Node<T>(node);
|
|
47
|
+
node.wildcard.name = (segment.slice(2) || unnamed++).toString();
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
node
|
|
50
|
+
node = node.wildcard;
|
|
51
|
+
type = WILDCARD;
|
|
52
|
+
}
|
|
53
|
+
// Static name
|
|
54
|
+
else {
|
|
55
|
+
let next: Node<T> | undefined = node.static?.get(segment);
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
node.children.set(PLACEHOLDER, child);
|
|
42
|
-
type = null;
|
|
57
|
+
if (!next) {
|
|
58
|
+
next = new Node<T>(node);
|
|
59
|
+
(node.static ??= new Map()).set(segment, next);
|
|
43
60
|
}
|
|
44
|
-
// "*:" Wildcard property
|
|
45
|
-
else if (symbol === '*') {
|
|
46
|
-
child.property = (segment.slice(2) || unnamed++).toString();
|
|
47
|
-
node.children.set(WILDCARD, child);
|
|
48
|
-
type = null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
node = next;
|
|
63
|
+
}
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
node.path = path;
|
|
@@ -64,77 +75,47 @@ class Node<T> {
|
|
|
64
75
|
route?: Readonly<Route<T>>;
|
|
65
76
|
} {
|
|
66
77
|
let node: Node<T> | undefined = this,
|
|
67
|
-
parameters: Record<PropertyKey, unknown>
|
|
78
|
+
parameters: Record<PropertyKey, unknown> | undefined,
|
|
68
79
|
segments = path.split('/'),
|
|
69
|
-
wildcard: { node: Node<T>,
|
|
80
|
+
wildcard: { node: Node<T>, start: number } | undefined;
|
|
70
81
|
|
|
71
82
|
for (let i = 0, n = segments.length; i < n; i++) {
|
|
72
|
-
let segment = segments[i]
|
|
73
|
-
wc = node.children?.get(WILDCARD);
|
|
83
|
+
let segment = segments[i];
|
|
74
84
|
|
|
75
|
-
if (
|
|
85
|
+
if (node.wildcard) {
|
|
76
86
|
wildcard = {
|
|
77
|
-
node:
|
|
78
|
-
|
|
87
|
+
node: node.wildcard,
|
|
88
|
+
start: i
|
|
79
89
|
};
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
// Exact matches take precedence over
|
|
83
|
-
let next: Node<T> | undefined = node.
|
|
92
|
+
// Exact matches take precedence over parameters
|
|
93
|
+
let next: Node<T> | undefined = node.static?.get(segment) as Node<T> | undefined;
|
|
84
94
|
|
|
85
95
|
if (next) {
|
|
86
96
|
node = next;
|
|
97
|
+
continue;
|
|
87
98
|
}
|
|
88
|
-
else {
|
|
89
|
-
node = node.children?.get(PLACEHOLDER);
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
parameters[ node.property! ] = segment;
|
|
100
|
+
if (!node.parameter) {
|
|
101
|
+
node = undefined;
|
|
102
|
+
break;
|
|
96
103
|
}
|
|
97
|
-
}
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
node =
|
|
101
|
-
parameters[ node.property! ] = wildcard.value;
|
|
105
|
+
node = node.parameter;
|
|
106
|
+
(parameters ??= {})[node.name!] = segment;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
109
|
+
if ((node === undefined || node.route === null) && wildcard) {
|
|
110
|
+
node = wildcard.node;
|
|
111
|
+
(parameters ??= {})[ node.name! ] = segments.slice(wildcard.start).join('/');
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
return {
|
|
109
115
|
parameters,
|
|
110
|
-
route: node
|
|
116
|
+
route: node?.route || undefined
|
|
111
117
|
};
|
|
112
118
|
}
|
|
113
|
-
|
|
114
|
-
remove(path: string) {
|
|
115
|
-
let node: Node<T> | undefined = this,
|
|
116
|
-
segments = path.split('/');
|
|
117
|
-
|
|
118
|
-
for (let i = 0, n = segments.length; i < n; i++) {
|
|
119
|
-
node = node.children?.get( segments[i] );
|
|
120
|
-
|
|
121
|
-
if (!node) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (node.children?.size) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
let parent = node.parent;
|
|
131
|
-
|
|
132
|
-
if (parent && parent.children) {
|
|
133
|
-
parent.children.delete( segments[segments.length - 1] );
|
|
134
|
-
parent.children.delete(WILDCARD);
|
|
135
|
-
parent.children.delete(PLACEHOLDER);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
119
|
}
|
|
139
120
|
|
|
140
121
|
|
package/src/types.ts
CHANGED
package/build/slugify.d.ts
DELETED
package/build/slugify.js
DELETED
package/src/slugify.ts
DELETED