@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.
@@ -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 (!request.hostname.startsWith(subdomains[i])) {
34
+ if (!hostname.startsWith(subdomains[i])) {
35
35
  continue;
36
36
  }
37
37
  subdomain = subdomains[i];
@@ -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 PLACEHOLDER = 0;
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, PLACEHOLDER, STATIC, WILDCARD };
8
+ export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
@@ -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 PLACEHOLDER = 0;
5
+ const PARAMETER = 0;
6
6
  const STATIC = 1;
7
7
  const WILDCARD = 2;
8
- export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PLACEHOLDER, STATIC, WILDCARD };
8
+ export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
package/build/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { default as browser } from './browser.js';
2
2
  export { default as router } from './router/index.js';
3
- export { default as slugify } from './slugify.js';
4
3
  export * from './types.js';
package/build/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { default as browser } from './browser.js';
2
2
  export { default as router } from './router/index.js';
3
- export { default as slugify } from './slugify.js';
4
3
  export * from './types.js';
@@ -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;
@@ -1,30 +1,31 @@
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
+ 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.at(-1) === '/') {
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
- route.pipeline.add(value[i]);
24
+ pipeline.add(value[i]);
24
25
  }
25
26
  }
26
27
  else if (key === 'responder') {
27
- route.pipeline.add(value);
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
- constructor() {
41
- this.root = new Node();
42
- }
43
- add(radixkey, route) {
44
- if (radixkey.indexOf(':') === -1 || this.root.add(radixkey, route).type === STATIC) {
45
- if (radixkey in this.static) {
46
- throw new Error(`Routing: static path '${radixkey}' is already in use`);
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
- this.static[radixkey] = route;
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 key = radixkey(method, path, subdomain);
88
- if (key in this.static) {
89
- return {
90
- route: this.static[key]
91
- };
91
+ let bucket = this.bucket[key(method, subdomain)];
92
+ if (!bucket) {
93
+ return {};
92
94
  }
93
- return this.root.find(key);
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
- if (route.name) {
98
- if (this.routes[route.name]) {
99
- throw new Error(`Routing: '${route.name}' is already in use`);
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[route.name] = route;
108
+ this.routes[name] = route;
102
109
  }
103
- if (route.path) {
110
+ if (path) {
104
111
  for (let i = 0, n = methods.length; i < n; i++) {
105
- let key = radixkey(methods[i], route.path, route.subdomain);
106
- if (key.indexOf('?:') !== -1) {
107
- let segments = key.split('?:'), url = '';
108
- for (let i = 0, n = segments.length; i < n; i++) {
109
- this.add((url += (i > 0 ? '/:' : '/') + segments[i]), route);
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(key, route);
122
+ this.add(method, path, route);
114
123
  }
115
124
  }
116
125
  }
117
- if (route.subdomain) {
118
- let subdomain = route.subdomain.toLowerCase();
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
  }
@@ -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 };
@@ -1,36 +1,44 @@
1
- import { PLACEHOLDER, STATIC, WILDCARD } from '../constants.js';
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 child = node.children?.get(segments[i]);
16
- if (!child) {
17
- let segment = segments[i], symbol = segment[0];
18
- if (!node.children) {
19
- node.children = new Map();
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.children.set(segment, (child = new Node(node)));
22
- if (symbol === ':') {
23
- child.property = (segment.slice(1) || unnamed++).toString();
24
- node.children.set(PLACEHOLDER, child);
25
- type = null;
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
- else if (symbol === '*') {
28
- child.property = (segment.slice(2) || unnamed++).toString();
29
- node.children.set(WILDCARD, child);
30
- type = null;
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 = {}, segments = path.split('/'), wildcard = null;
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], wc = node.children?.get(WILDCARD);
44
- if (wc) {
51
+ let segment = segments[i];
52
+ if (node.wildcard) {
45
53
  wildcard = {
46
- node: wc,
47
- value: segments.slice(i).join('/')
54
+ node: node.wildcard,
55
+ start: i
48
56
  };
49
57
  }
50
- let next = node.children?.get(segment);
58
+ let next = node.static?.get(segment);
51
59
  if (next) {
52
60
  node = next;
61
+ continue;
53
62
  }
54
- else {
55
- node = node.children?.get(PLACEHOLDER);
56
- if (!node) {
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 !== null) {
70
+ if ((node === undefined || node.route === null) && wildcard) {
63
71
  node = wildcard.node;
64
- parameters[node.property] = wildcard.value;
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.route
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.21.1"
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.32",
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 subdomains = router.subdomains;
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 (!request.hostname.startsWith(subdomains[i])) {
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 PLACEHOLDER = 0;
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, PLACEHOLDER, STATIC, WILDCARD };
17
+ export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { default as browser } from './browser';
2
2
  export { default as router } from './router';
3
- export { default as slugify } from './slugify';
4
3
  export * from './types';
@@ -1,16 +1,20 @@
1
- import { ON_DELETE, ON_GET, ON_POST, ON_PUT, STATIC } from '~/constants';
2
- import { Name, Options, Request, Route, RouteOptions } from '~/types';
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.at(-1) === '/') {
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
- route.pipeline.add(value[i]);
33
+ pipeline.add(value[i]);
32
34
  }
33
35
  }
34
36
  else if (key === 'responder') {
35
- route.pipeline.add(value);
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
- constructor() {
54
- this.root = new Node();
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
- private add(radixkey: string, route: Route<T>) {
59
- if (radixkey.indexOf(':') === -1 || this.root.add(radixkey, route).type === STATIC) {
60
- if (radixkey in this.static) {
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
- this.static[radixkey] = route;
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 key = radixkey(method, path, subdomain);
120
+ let bucket = this.bucket[ key(method, subdomain) ];
117
121
 
118
- if (key in this.static) {
119
- return {
120
- route: this.static[key] as Readonly<Route<T>>
121
- };
122
+ if (!bucket) {
123
+ return {};
122
124
  }
123
125
 
124
- return this.root.find(key);
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
- if (route.name) {
131
- if (this.routes[route.name]) {
132
- throw new Error(`Routing: '${route.name}' is already in use`);
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[route.name] = route;
147
+ this.routes[name] = route;
136
148
  }
137
149
 
138
- if (route.path) {
150
+ if (path) {
139
151
  for (let i = 0, n = methods.length; i < n; i++) {
140
- let key = radixkey(methods[i], route.path, route.subdomain);
152
+ let method = methods[i];
141
153
 
142
- if (key.indexOf('?:') !== -1) {
143
- let segments = key.split('?:'),
144
- url = '';
154
+ if (path.indexOf('?:') !== -1) {
155
+ let segments = path.split('?:'),
156
+ url = segments[0];
145
157
 
146
- for (let i = 0, n = segments.length; i < n; i++) {
147
- this.add((url += (i > 0 ? '/:' : '/') + segments[i]), route);
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(key, route);
166
+ this.add(method, path, route);
152
167
  }
153
168
  }
154
169
  }
155
170
 
156
- if (route.subdomain) {
157
- let subdomain = route.subdomain.toLowerCase();
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;
@@ -1,15 +1,19 @@
1
- import { PLACEHOLDER, STATIC, WILDCARD } from '~/constants';
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 child: Node<T> | undefined = node.children?.get(segments[i]);
30
+ let segment = segments[i],
31
+ symbol = segment[0];
27
32
 
28
- if (!child) {
29
- let segment = segments[i],
30
- symbol = segment[0];
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
- if (!node.children) {
33
- node.children = new Map();
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.children.set(segment, (child = new Node<T>(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
- // Named property
39
- if (symbol === ':') {
40
- child.property = (segment.slice(1) || unnamed++).toString();
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
- node = child;
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>, value: string } | null = null;
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 (wc) {
85
+ if (node.wildcard) {
76
86
  wildcard = {
77
- node: wc,
78
- value: segments.slice(i).join('/')
87
+ node: node.wildcard,
88
+ start: i
79
89
  };
80
90
  }
81
91
 
82
- // Exact matches take precedence over placeholders
83
- let next: Node<T> | undefined = node.children?.get(segment);
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
- if (!node) {
92
- break;
93
- }
94
-
95
- parameters[ node.property! ] = segment;
100
+ if (!node.parameter) {
101
+ node = undefined;
102
+ break;
96
103
  }
97
- }
98
104
 
99
- if ((node === undefined || node.route === null) && wildcard !== null) {
100
- node = wildcard.node;
101
- parameters[ node.property! ] = wildcard.value;
105
+ node = node.parameter;
106
+ (parameters ??= {})[node.name!] = segment;
102
107
  }
103
108
 
104
- if (!node) {
105
- return {};
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.route!
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
@@ -41,4 +41,9 @@ type RouteOptions<T> = Options<T> & {
41
41
  };
42
42
 
43
43
 
44
- export type { Middleware, Name, Next, Options, Request, Route, RouteOptions, Router };
44
+ export type {
45
+ Middleware,
46
+ Name, Next,
47
+ Options,
48
+ Request, Route, RouteOptions, Router
49
+ };
@@ -1,2 +0,0 @@
1
- declare const _default: (value: string) => string;
2
- export default _default;
package/build/slugify.js DELETED
@@ -1,3 +0,0 @@
1
- export default (value) => {
2
- return value.replace(/\W+/g, '-').replace(/[-]+$/, '').toLowerCase();
3
- };
package/src/slugify.ts DELETED
@@ -1,4 +0,0 @@
1
- // https://twitter.com/Swizec/status/1589416111971635201
2
- export default (value: string) => {
3
- return value.replace(/\W+/g, '-').replace(/[-]+$/, '').toLowerCase();
4
- };