@esportsplus/routing 0.2.0 → 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/router/index.js +6 -3
- package/build/router/node.js +2 -2
- package/package.json +2 -2
- package/src/router/index.ts +6 -3
- package/src/router/node.ts +5 -5
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/router/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
4
|
function key(method, subdomain) {
|
|
5
|
-
return (method + (subdomain ?
|
|
5
|
+
return (method + (subdomain ? ' ' + subdomain : '')).toUpperCase();
|
|
6
6
|
}
|
|
7
7
|
function normalize(path) {
|
|
8
8
|
if (path) {
|
|
@@ -42,12 +42,15 @@ class Router {
|
|
|
42
42
|
root: new Node(),
|
|
43
43
|
static: {}
|
|
44
44
|
};
|
|
45
|
-
if (path.indexOf(':') === -1
|
|
45
|
+
if (path.indexOf(':') === -1) {
|
|
46
46
|
if (path in bucket.static) {
|
|
47
47
|
throw new Error(`Routing: static path '${path}' is already in use`);
|
|
48
48
|
}
|
|
49
49
|
bucket.static[path] = route;
|
|
50
50
|
}
|
|
51
|
+
else {
|
|
52
|
+
bucket.root.add(path, route);
|
|
53
|
+
}
|
|
51
54
|
return this;
|
|
52
55
|
}
|
|
53
56
|
route(options) {
|
package/build/router/node.js
CHANGED
|
@@ -52,7 +52,7 @@ class Node {
|
|
|
52
52
|
if (node.wildcard) {
|
|
53
53
|
wildcard = {
|
|
54
54
|
node: node.wildcard,
|
|
55
|
-
|
|
55
|
+
start: i
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
let next = node.static?.get(segment);
|
|
@@ -69,7 +69,7 @@ class Node {
|
|
|
69
69
|
}
|
|
70
70
|
if ((node === undefined || node.route === null) && wildcard) {
|
|
71
71
|
node = wildcard.node;
|
|
72
|
-
(parameters ??= {})[node.name] = wildcard.
|
|
72
|
+
(parameters ??= {})[node.name] = segments.slice(wildcard.start).join('/');
|
|
73
73
|
}
|
|
74
74
|
return {
|
|
75
75
|
parameters,
|
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.22.
|
|
6
|
+
"@esportsplus/utilities": "^0.22.1"
|
|
7
7
|
},
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"@esportsplus/typescript": "^0.9.2"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"types": "./build/index.d.ts",
|
|
17
|
-
"version": "0.2.
|
|
17
|
+
"version": "0.2.1",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc && tsc-alias",
|
|
20
20
|
"-": "-",
|
package/src/router/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { ON_DELETE, ON_GET, ON_POST, ON_PUT
|
|
1
|
+
import { ON_DELETE, ON_GET, ON_POST, ON_PUT } from '../constants';
|
|
2
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
7
|
function key(method: string, subdomain?: string | null) {
|
|
8
|
-
return (method + (subdomain ?
|
|
8
|
+
return (method + (subdomain ? ' ' + subdomain : '')).toUpperCase();
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function normalize(path: string) {
|
|
@@ -57,13 +57,16 @@ class Router<T> {
|
|
|
57
57
|
static: {}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
if (path.indexOf(':') === -1
|
|
60
|
+
if (path.indexOf(':') === -1) {
|
|
61
61
|
if (path in bucket.static) {
|
|
62
62
|
throw new Error(`Routing: static path '${path}' is already in use`);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
bucket.static[path] = route;
|
|
66
66
|
}
|
|
67
|
+
else {
|
|
68
|
+
bucket.root.add(path, route);
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
return this;
|
|
69
72
|
}
|
package/src/router/node.ts
CHANGED
|
@@ -30,7 +30,7 @@ class Node<T> {
|
|
|
30
30
|
let segment = segments[i],
|
|
31
31
|
symbol = segment[0];
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Parameter
|
|
34
34
|
if (symbol === ':') {
|
|
35
35
|
if (!node.parameter) {
|
|
36
36
|
node.parameter = new Node<T>(node);
|
|
@@ -40,7 +40,7 @@ class Node<T> {
|
|
|
40
40
|
node = node.parameter;
|
|
41
41
|
type = PARAMETER;
|
|
42
42
|
}
|
|
43
|
-
// "*:" Wildcard
|
|
43
|
+
// "*:" Wildcard
|
|
44
44
|
else if (symbol === '*') {
|
|
45
45
|
if (!node.wildcard) {
|
|
46
46
|
node.wildcard = new Node<T>(node);
|
|
@@ -77,7 +77,7 @@ class Node<T> {
|
|
|
77
77
|
let node: Node<T> | undefined = this,
|
|
78
78
|
parameters: Record<PropertyKey, unknown> | undefined,
|
|
79
79
|
segments = path.split('/'),
|
|
80
|
-
wildcard: { node: Node<T>,
|
|
80
|
+
wildcard: { node: Node<T>, start: number } | undefined;
|
|
81
81
|
|
|
82
82
|
for (let i = 0, n = segments.length; i < n; i++) {
|
|
83
83
|
let segment = segments[i];
|
|
@@ -85,7 +85,7 @@ class Node<T> {
|
|
|
85
85
|
if (node.wildcard) {
|
|
86
86
|
wildcard = {
|
|
87
87
|
node: node.wildcard,
|
|
88
|
-
|
|
88
|
+
start: i
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -108,7 +108,7 @@ class Node<T> {
|
|
|
108
108
|
|
|
109
109
|
if ((node === undefined || node.route === null) && wildcard) {
|
|
110
110
|
node = wildcard.node;
|
|
111
|
-
(parameters ??= {})[ node.name! ] = wildcard.
|
|
111
|
+
(parameters ??= {})[ node.name! ] = segments.slice(wildcard.start).join('/');
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return {
|