@esportsplus/routing 0.2.0 → 0.2.2

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.
@@ -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
+ })();
@@ -1,8 +1,8 @@
1
- import { ON_DELETE, ON_GET, ON_POST, ON_PUT, STATIC } from '../constants.js';
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 ? subdomain + ' ' : '')).toUpperCase();
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 || bucket.root.add(path, route).type === STATIC) {
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) {
@@ -52,7 +52,7 @@ class Node {
52
52
  if (node.wildcard) {
53
53
  wildcard = {
54
54
  node: node.wildcard,
55
- value: segments.slice(i).join('/')
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.value;
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.0"
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.0",
17
+ "version": "0.2.2",
18
18
  "scripts": {
19
19
  "build": "tsc && tsc-alias",
20
20
  "-": "-",
@@ -1,11 +1,11 @@
1
- import { ON_DELETE, ON_GET, ON_POST, ON_PUT, STATIC } from '../constants';
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 ? subdomain + ' ' : '')).toUpperCase();
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 || bucket.root.add(path, route).type === STATIC) {
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
  }
@@ -30,7 +30,7 @@ class Node<T> {
30
30
  let segment = segments[i],
31
31
  symbol = segment[0];
32
32
 
33
- // Named name
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 name
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>, value: string } | undefined;
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
- value: segments.slice(i).join('/')
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.value;
111
+ (parameters ??= {})[ node.name! ] = segments.slice(wildcard.start).join('/');
112
112
  }
113
113
 
114
114
  return {