rax-radix-tree 1.0.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/LICENSE +21 -0
- package/README.md +574 -0
- package/benchmark/01.match-equals.js +21 -0
- package/benchmark/02.match-suffix.js +20 -0
- package/benchmark/03.match-prefix.js +28 -0
- package/benchmark/04.match-equals-prefix-suffix.js +36 -0
- package/benchmark/05.match-hosts.js +25 -0
- package/benchmark/06.match-wildcard-hosts.js +25 -0
- package/benchmark/07.match-remote-address.js +40 -0
- package/benchmark/08.match-exprs-headers.js +50 -0
- package/benchmark/09.match-exprs-args.js +50 -0
- package/benchmark/run.sh +3 -0
- package/benchmark/utility.js +56 -0
- package/binding.gyp +14 -0
- package/http-radix-tree.js +150 -0
- package/index.js +5 -0
- package/package.json +38 -0
- package/radix-tree.js +284 -0
- package/src/radix_tree.c +169 -0
- package/src/radix_tree.h +39 -0
- package/src/rax/COPYING +9 -0
- package/src/rax/Makefile +26 -0
- package/src/rax/README.md +488 -0
- package/src/rax/crc16.c +88 -0
- package/src/rax/rax-oom-test.c +175 -0
- package/src/rax/rax-test.c +1065 -0
- package/src/rax/rax.c +1996 -0
- package/src/rax/rax.h +217 -0
- package/src/rax/rax_malloc.h +43 -0
- package/src/rax/rax_oom_malloc.h +63 -0
- package/src/rax/rc4rand.c +77 -0
- package/src/rax/rc4rand.h +9 -0
- package/src/rax_wrapper.cpp +238 -0
- package/test/00.path.test.js +84 -0
- package/test/10.tree.test.js +217 -0
- package/test/20.http-tree.test.js +316 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 igeeky.io@gmail.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# Name
|
|
2
|
+
|
|
3
|
+
A high-performance radix tree implementation for Node.js, providing efficient routing and pattern matching capabilities. This library is built on top of the rax radix tree implementation in ANSI C (https://github.com/antirez/rax), offering excellent performance and scalability for large-scale applications.
|
|
4
|
+
|
|
5
|
+
# Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Name](#name)
|
|
8
|
+
- [Table of Contents](#table-of-contents)
|
|
9
|
+
- [Synopsis](#synopsis)
|
|
10
|
+
- [RadixTree](#radixtree)
|
|
11
|
+
- [HttpRadixTree](#httpradixtree)
|
|
12
|
+
- [Class Methods](#class-methods)
|
|
13
|
+
- [RadixTree](#radixtree-1)
|
|
14
|
+
- [constructor](#constructor)
|
|
15
|
+
- [findAllRoutes](#findallroutes)
|
|
16
|
+
- [findRoute](#findroute)
|
|
17
|
+
- [HttpRadixTree](#httpradixtree-1)
|
|
18
|
+
- [constructor](#constructor-1)
|
|
19
|
+
- [Examples](#examples)
|
|
20
|
+
- [Full Path Match](#full-path-match)
|
|
21
|
+
- [Prefix Match](#prefix-match)
|
|
22
|
+
- [Suffix Match](#suffix-match)
|
|
23
|
+
- [Domain Matching](#domain-matching)
|
|
24
|
+
- [Remote Address Matching](#remote-address-matching)
|
|
25
|
+
- [Expression Matching (GET Parameters)](#expression-matching-get-parameters)
|
|
26
|
+
- [Expression Matching (Request Headers)](#expression-matching-request-headers)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Benchmarks](#benchmarks)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
# Synopsis
|
|
32
|
+
|
|
33
|
+
## RadixTree
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const { RadixTree } = require('rax-radix-tree');
|
|
37
|
+
|
|
38
|
+
// Define routes
|
|
39
|
+
const routes = [
|
|
40
|
+
{ paths: ['/api/users'], meta: { id: 1 } },
|
|
41
|
+
{ paths: ['/api/users/*'], meta: { id: 2 } },
|
|
42
|
+
{ paths: ['/api/posts/**'], meta: { id: 3 } },
|
|
43
|
+
{ paths: ['**.jpg', '*.png'], meta: { id: 4 } },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Create RadixTree instance
|
|
47
|
+
const tree = new RadixTree(routes);
|
|
48
|
+
|
|
49
|
+
// Find matching routes
|
|
50
|
+
const result1 = tree.findAllRoutes('/api/users');
|
|
51
|
+
console.log(result1); // [{ meta: { id: 1 } }]
|
|
52
|
+
|
|
53
|
+
const result2 = tree.findAllRoutes('/api/users/123');
|
|
54
|
+
console.log(result2); // [{ meta: { id: 2 } }]
|
|
55
|
+
|
|
56
|
+
const result3 = tree.findAllRoutes('/api/posts/2023/05/01');
|
|
57
|
+
console.log(result3); // [{ meta: { id: 3 } }]
|
|
58
|
+
|
|
59
|
+
const result4 = tree.findAllRoutes('/images/photo.jpg');
|
|
60
|
+
console.log(result4); // [{ meta: { id: 4 } }]
|
|
61
|
+
|
|
62
|
+
// Use findRoute method (requires passing an object containing path)
|
|
63
|
+
const matchedRoute = tree.findRoute({ path: '/api/users/123', method: 'GET' });
|
|
64
|
+
console.log(matchedRoute); // { meta: { id: 2 } }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## HttpRadixTree
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const { HttpRadixTree } = require('rax-radix-tree');
|
|
71
|
+
|
|
72
|
+
// Define HTTP routes
|
|
73
|
+
const httpRoutes = [
|
|
74
|
+
{ paths: ['/api/**'], meta: { id: 1 }, hosts: ['example.com'] },
|
|
75
|
+
{ paths: ['/api/users/**'], meta: { id: 2 }, methods: ['GET', 'POST'] },
|
|
76
|
+
{ paths: ['/api/admin/**'], meta: { id: 3 }, remoteAddrs: ['127.0.0.1', '192.168.1.0/24'] },
|
|
77
|
+
{ paths: ['/api/products/**'], meta: { id: 4 }, exprs: ['category == "electronics" and price > 100'] },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Create HttpRadixTree instance
|
|
81
|
+
const httpTree = new HttpRadixTree(httpRoutes);
|
|
82
|
+
|
|
83
|
+
// Find matching HTTP routes
|
|
84
|
+
const httpResult1 = httpTree.findRoute({
|
|
85
|
+
path: '/api/users/123',
|
|
86
|
+
method: 'GET',
|
|
87
|
+
headers: { host: 'example.com' }
|
|
88
|
+
});
|
|
89
|
+
console.log(httpResult1); // { meta: { id: 2 } }
|
|
90
|
+
|
|
91
|
+
const httpResult2 = httpTree.findRoute({
|
|
92
|
+
path: '/api/admin/dashboard',
|
|
93
|
+
method: 'GET',
|
|
94
|
+
remoteAddr: '127.0.0.1'
|
|
95
|
+
});
|
|
96
|
+
console.log(httpResult2); // { meta: { id: 3 } }
|
|
97
|
+
|
|
98
|
+
const httpResult3 = httpTree.findRoute({
|
|
99
|
+
path: '/api/products/laptop',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
args: { category: 'electronics', price: '599.99' }
|
|
102
|
+
});
|
|
103
|
+
console.log(httpResult3); // { meta: { id: 4 } }
|
|
104
|
+
|
|
105
|
+
// Non-matching case
|
|
106
|
+
const httpResult4 = httpTree.findRoute({
|
|
107
|
+
path: '/api/users/123',
|
|
108
|
+
method: 'PUT',
|
|
109
|
+
headers: { host: 'other-domain.com' }
|
|
110
|
+
});
|
|
111
|
+
console.log(httpResult4); // null
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
[Back to TOC](#table-of-contents)
|
|
115
|
+
|
|
116
|
+
# Class Methods
|
|
117
|
+
|
|
118
|
+
## RadixTree
|
|
119
|
+
|
|
120
|
+
### constructor
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
constructor(routes: Route[], matchFunc?: MatchFunction)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Creates a new RadixTree instance.
|
|
127
|
+
|
|
128
|
+
Parameters:
|
|
129
|
+
|
|
130
|
+
- `routes`: Array of route configurations
|
|
131
|
+
- `matchFunc`: Optional custom matching function
|
|
132
|
+
|
|
133
|
+
Each route can contain the following properties:
|
|
134
|
+
|
|
135
|
+
| Name | Required? | Description | Example |
|
|
136
|
+
|---------|-----------|-----------------------------------------------------------------------|------------------------------|
|
|
137
|
+
| paths | Required | List of request paths to match. By default, performs full matching. Adding `*` at the end will perform prefix matching, but not including '/' characters. Adding `**` will perform arbitrary matching, matching all characters. For example, `/foo*` can match `/foobar`, but not `/foo/bar`. `/foo**` can match both `/foo/bar` and `/foo/car/far`. Adding a suffix pattern like `**.jpg` will match all paths ending with `.jpg`. For instance, `**.jpg` would match `/images/photo.jpg` and `/uploads/profile.jpg`. | `["/", "/foo", "/bar/*", "/baz/**", "**.jpg"]` |
|
|
138
|
+
| methods | Optional | List of HTTP methods to match. Valid values: "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE", and "ALL". If not specified, defaults to "ALL". | `["GET", "POST"]` |
|
|
139
|
+
| meta | Optional | Metadata to be returned when the route matches. | `{ id: 1 }` |
|
|
140
|
+
|
|
141
|
+
### findAllRoutes
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
findAllRoutes(path: string, method?: string): Array<Route>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Finds all routes that match the given path and method.
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
|
|
151
|
+
- `path`: The path to match against
|
|
152
|
+
- `method`: (Optional) The HTTP method to match
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
|
|
156
|
+
- An array of matching routes
|
|
157
|
+
|
|
158
|
+
### findRoute
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
findRoute(req: { path: string, method?: string, [key: string]: any }): Route | null
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Finds the first route that matches the given request object.
|
|
165
|
+
|
|
166
|
+
Parameters:
|
|
167
|
+
|
|
168
|
+
- `req`: A request object containing path, method, and other optional properties
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
|
|
172
|
+
- The matching route object if found, or null if no match is found
|
|
173
|
+
|
|
174
|
+
## HttpRadixTree
|
|
175
|
+
|
|
176
|
+
### constructor
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
constructor(routes: HttpRoute[])
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Creates a new HttpRadixTree instance.
|
|
183
|
+
|
|
184
|
+
Parameters:
|
|
185
|
+
|
|
186
|
+
- `routes`: Array of HTTP route configurations
|
|
187
|
+
|
|
188
|
+
Each HTTP route can contain the following properties:
|
|
189
|
+
|
|
190
|
+
| Name | Required? | Description | Example |
|
|
191
|
+
|-------------|-----------|------------------------------------------------------------------------------------------------|---------------------------------------------|
|
|
192
|
+
| paths | Required | List of request paths to match. Supports the same matching rules as RadixTree. | `["/api/**", "/users/*"]` |
|
|
193
|
+
| methods | Optional | List of HTTP methods to match. Same as RadixTree. | `["GET", "POST"]` |
|
|
194
|
+
| hosts | Optional | List of host addresses to match. Supports wildcards. For example, `*.example.com` can match `foo.example.com` and `bar.example.com`. | `["example.com", "*.api.com"]` |
|
|
195
|
+
| remoteAddrs | Optional | List of remote addresses (IPv4 or IPv6) to match. Supports CIDR format. | `["127.0.0.1", "192.168.1.0/24", "::1", "fe80::/10"]` |
|
|
196
|
+
| exprs | Optional | List of expressions to evaluate request parameters. Uses [filtrex](https://github.com/joewalnes/filtrex) syntax. | `["age >= 18", "category in ('electronics', 'books')"]` |
|
|
197
|
+
| meta | Optional | Metadata to be returned when the route matches. | `{ id: 1 }` |
|
|
198
|
+
|
|
199
|
+
HttpRadixTree adds support for matching hosts, remote addresses, and custom expressions on top of RadixTree, making it more suitable for HTTP routing scenarios.
|
|
200
|
+
|
|
201
|
+
Both classes provide powerful route matching capabilities and can be chosen based on different needs. RadixTree is suitable for general path matching scenarios, while HttpRadixTree is more suitable for complex HTTP routing matching requirements.
|
|
202
|
+
|
|
203
|
+
[Back to TOC](#table-of-contents)
|
|
204
|
+
|
|
205
|
+
# Examples
|
|
206
|
+
|
|
207
|
+
## Full Path Match
|
|
208
|
+
|
|
209
|
+
Demonstrates basic full path matching:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
const { RadixTree } = require('rax-radix-tree');
|
|
213
|
+
|
|
214
|
+
const routes = [
|
|
215
|
+
{ paths: ['/users'], meta: { handler: 'listUsers' } },
|
|
216
|
+
{ paths: ['/users/create'], meta: { handler: 'createUser' } },
|
|
217
|
+
{ paths: ['/posts'], meta: { handler: 'listPosts' } }
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const tree = new RadixTree(routes);
|
|
221
|
+
|
|
222
|
+
console.log(tree.findRoute({ path: '/users' }));
|
|
223
|
+
// Output: { meta: { handler: 'listUsers' } }
|
|
224
|
+
|
|
225
|
+
console.log(tree.findRoute({ path: '/users/create' }));
|
|
226
|
+
// Output: { meta: { handler: 'createUser' } }
|
|
227
|
+
|
|
228
|
+
console.log(tree.findRoute({ path: '/posts' }));
|
|
229
|
+
// Output: { meta: { handler: 'listPosts' } }
|
|
230
|
+
|
|
231
|
+
console.log(tree.findRoute({ path: '/unknown' }));
|
|
232
|
+
// Output: null
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Prefix Match
|
|
236
|
+
|
|
237
|
+
Shows how to use wildcard characters for prefix matching:
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
const { RadixTree } = require('rax-radix-tree');
|
|
242
|
+
|
|
243
|
+
const routes = [
|
|
244
|
+
{ paths: ['/api/*'], meta: { access: 'public' } },
|
|
245
|
+
{ paths: ['/admin/**'], meta: { access: 'private' } }
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
const tree = new RadixTree(routes);
|
|
249
|
+
|
|
250
|
+
console.log(tree.findRoute({ path: '/api/users' }));
|
|
251
|
+
// Output: { meta: { access: 'public' } }
|
|
252
|
+
|
|
253
|
+
console.log(tree.findRoute({ path: '/api/posts/recent' }));
|
|
254
|
+
// Output: null (because '/api/*' only matches one level)
|
|
255
|
+
|
|
256
|
+
console.log(tree.findRoute({ path: '/admin/dashboard' }));
|
|
257
|
+
// Output: { meta: { access: 'private' } }
|
|
258
|
+
|
|
259
|
+
console.log(tree.findRoute({ path: '/admin/users/edit' }));
|
|
260
|
+
// Output: { meta: { access: 'private' } }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Suffix Match
|
|
264
|
+
|
|
265
|
+
Illustrates suffix matching using double wildcards:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
const { RadixTree } = require('rax-radix-tree');
|
|
269
|
+
|
|
270
|
+
const routes = [
|
|
271
|
+
{ paths: ['**.jpg', '**.png'], meta: { type: 'image' } },
|
|
272
|
+
{ paths: ['**.html'], meta: { type: 'webpage' } }
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const tree = new RadixTree(routes);
|
|
276
|
+
|
|
277
|
+
console.log(tree.findRoute({ path: '/images/photo.jpg' }));
|
|
278
|
+
// Output: { meta: { type: 'image' } }
|
|
279
|
+
|
|
280
|
+
console.log(tree.findRoute({ path: '/assets/icon.png' }));
|
|
281
|
+
// Output: { meta: { type: 'image' } }
|
|
282
|
+
|
|
283
|
+
console.log(tree.findRoute({ path: '/index.html' }));
|
|
284
|
+
// Output: { meta: { type: 'webpage' } }
|
|
285
|
+
|
|
286
|
+
console.log(tree.findRoute({ path: '/document.pdf' }));
|
|
287
|
+
// Output: null
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Domain Matching
|
|
291
|
+
|
|
292
|
+
Demonstrates domain matching using HttpRadixTree:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
const { HttpRadixTree } = require('rax-radix-tree');
|
|
296
|
+
|
|
297
|
+
const routes = [
|
|
298
|
+
{ paths: ['/**'], hosts: ['example.com'], meta: { site: 'main' } },
|
|
299
|
+
{ paths: ['/**'], hosts: ['*.example.com'], meta: { site: 'subdomain' } },
|
|
300
|
+
{ paths: ['/**'], hosts: ['api.example.com'], meta: { site: 'api' } }
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const tree = new HttpRadixTree(routes);
|
|
304
|
+
|
|
305
|
+
console.log(tree.findRoute({ path: '/', headers: { host: 'example.com' } }));
|
|
306
|
+
// Output: { meta: { site: 'main' } }
|
|
307
|
+
|
|
308
|
+
console.log(tree.findRoute({ path: '/about', headers: { host: 'blog.example.com' } }));
|
|
309
|
+
// Output: { meta: { site: 'subdomain' } }
|
|
310
|
+
|
|
311
|
+
console.log(tree.findRoute({ path: '/users', headers: { host: 'api.example.com' } }));
|
|
312
|
+
// Output: { meta: { site: 'api' } }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Remote Address Matching
|
|
316
|
+
|
|
317
|
+
Shows how to match routes based on remote IP addresses:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
const { HttpRadixTree } = require('rax-radix-tree');
|
|
321
|
+
|
|
322
|
+
const routes = [
|
|
323
|
+
{ paths: ['/admin/**'], remoteAddrs: ['127.0.0.1'], meta: { access: 'local' } },
|
|
324
|
+
{ paths: ['/api/**'], remoteAddrs: ['192.168.1.0/24'], meta: { access: 'lan' } },
|
|
325
|
+
{ paths: ['/**'], meta: { access: 'public' } }
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const tree = new HttpRadixTree(routes);
|
|
329
|
+
|
|
330
|
+
console.log(tree.findRoute({ path: '/admin/dashboard', remoteAddr: '127.0.0.1' }));
|
|
331
|
+
// Output: { meta: { access: 'local' } }
|
|
332
|
+
|
|
333
|
+
console.log(tree.findRoute({ path: '/api/users', remoteAddr: '192.168.1.100' }));
|
|
334
|
+
// Output: { meta: { access: 'lan' } }
|
|
335
|
+
|
|
336
|
+
console.log(tree.findRoute({ path: '/', remoteAddr: '203.0.113.1' }));
|
|
337
|
+
// Output: { meta: { access: 'public' } }
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Expression Matching (GET Parameters)
|
|
341
|
+
|
|
342
|
+
Illustrates how to use expressions for matching GET parameters:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const { HttpRadixTree } = require('rax-radix-tree');
|
|
346
|
+
|
|
347
|
+
const routes = [
|
|
348
|
+
{ paths: ['/products/**'], exprs: ['category == "electronics" and price > 100'], meta: { discount: '10%' } },
|
|
349
|
+
{ paths: ['/products/**'], exprs: ['category == "books" and quantity >= 5'], meta: { discount: '20%' } },
|
|
350
|
+
{ paths: ['/products/**'], meta: { discount: '0%' } }
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const tree = new HttpRadixTree(routes);
|
|
354
|
+
|
|
355
|
+
console.log(tree.findRoute({
|
|
356
|
+
path: '/products/laptop',
|
|
357
|
+
args: { category: 'electronics', price: '999.99' }
|
|
358
|
+
}));
|
|
359
|
+
// Output: { meta: { discount: '10%' } }
|
|
360
|
+
|
|
361
|
+
console.log(tree.findRoute({
|
|
362
|
+
path: '/products/novel',
|
|
363
|
+
args: { category: 'books', quantity: '7' }
|
|
364
|
+
}));
|
|
365
|
+
// Output: { meta: { discount: '20%' } }
|
|
366
|
+
|
|
367
|
+
console.log(tree.findRoute({
|
|
368
|
+
path: '/products/pencil',
|
|
369
|
+
args: { category: 'stationery', price: '1.99' }
|
|
370
|
+
}));
|
|
371
|
+
// Output: { meta: { discount: '0%' } }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Expression Matching (Request Headers)
|
|
375
|
+
|
|
376
|
+
Demonstrates expression matching based on request headers:
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
const { HttpRadixTree } = require('rax-radix-tree');
|
|
380
|
+
|
|
381
|
+
const routes = [
|
|
382
|
+
{ paths: ['/**'], exprs: ['contains(user_agent, "Mozilla") and accept_language == "en-US"'], meta: { version: 'v1' } },
|
|
383
|
+
{ paths: ['/**'], exprs: ['contains(user_agent, "Chrome") and accept_language == "zh-CN"'], meta: { version: 'v2' } },
|
|
384
|
+
{ paths: ['/**'], meta: { version: 'default' } }
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const tree = new HttpRadixTree(routes);
|
|
388
|
+
|
|
389
|
+
console.log(tree.findRoute({
|
|
390
|
+
path: '/',
|
|
391
|
+
headers: {
|
|
392
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
393
|
+
'accept-language': 'en-US'
|
|
394
|
+
}
|
|
395
|
+
}));
|
|
396
|
+
// Output: { meta: { version: 'v1' } }
|
|
397
|
+
|
|
398
|
+
console.log(tree.findRoute({
|
|
399
|
+
path: '/about',
|
|
400
|
+
headers: {
|
|
401
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
402
|
+
'accept-language': 'zh-CN'
|
|
403
|
+
}
|
|
404
|
+
}));
|
|
405
|
+
// Output: { meta: { version: 'v2' } }
|
|
406
|
+
|
|
407
|
+
console.log(tree.findRoute({
|
|
408
|
+
path: '/contact',
|
|
409
|
+
headers: {
|
|
410
|
+
'user-agent': 'curl/7.64.1',
|
|
411
|
+
'accept-language': 'fr-FR'
|
|
412
|
+
}
|
|
413
|
+
}));
|
|
414
|
+
// Output: { meta: { version: 'default' } }
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
[Back to TOC](#table-of-contents)
|
|
418
|
+
|
|
419
|
+
# Installation
|
|
420
|
+
|
|
421
|
+
```shell
|
|
422
|
+
npm install rax-radix-tree
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# Benchmarks
|
|
427
|
+
|
|
428
|
+
Benchmark test cases are located in the [benchmark](./benchmark) directory.
|
|
429
|
+
|
|
430
|
+
Environment: MacBook Pro (2023), Apple M3 Max
|
|
431
|
+
|
|
432
|
+
To start benchmarking, run:
|
|
433
|
+
|
|
434
|
+
```shell
|
|
435
|
+
sh benchmark/run.sh
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Result:
|
|
439
|
+
|
|
440
|
+
```markdown
|
|
441
|
+
## Match equals
|
|
442
|
+
|
|
443
|
+
* Result: equals_50000
|
|
444
|
+
* Number of routes: 100000
|
|
445
|
+
* Number of matches: 10000000
|
|
446
|
+
* Execution time: 492 ms
|
|
447
|
+
* QPS: 20320429
|
|
448
|
+
|
|
449
|
+
## Match suffix
|
|
450
|
+
|
|
451
|
+
* Result: suffix_50000
|
|
452
|
+
* Number of routes: 100000
|
|
453
|
+
* Number of matches: 1000000
|
|
454
|
+
* Execution time: 2851 ms
|
|
455
|
+
* QPS: 350786
|
|
456
|
+
|
|
457
|
+
## Match prefix, route found
|
|
458
|
+
|
|
459
|
+
* Result: prefix_50000
|
|
460
|
+
* Number of routes: 100000
|
|
461
|
+
* Number of matches: 1000000
|
|
462
|
+
* Execution time: 2302 ms
|
|
463
|
+
* QPS: 434428
|
|
464
|
+
|
|
465
|
+
## Match prefix, route not found
|
|
466
|
+
|
|
467
|
+
* Result: null
|
|
468
|
+
* Number of routes: 100000
|
|
469
|
+
* Number of matches: 1000000
|
|
470
|
+
* Execution time: 2231 ms
|
|
471
|
+
* QPS: 448280
|
|
472
|
+
|
|
473
|
+
## Equal match in combined tree
|
|
474
|
+
|
|
475
|
+
* Result: equals_50000
|
|
476
|
+
* Number of routes: 300000
|
|
477
|
+
* Number of matches: 100000
|
|
478
|
+
* Execution time: 372 ms
|
|
479
|
+
* QPS: 269047
|
|
480
|
+
|
|
481
|
+
## Prefix match in combined tree
|
|
482
|
+
|
|
483
|
+
* Result: prefix_50000
|
|
484
|
+
* Number of routes: 300000
|
|
485
|
+
* Number of matches: 100000
|
|
486
|
+
* Execution time: 453 ms
|
|
487
|
+
* QPS: 220543
|
|
488
|
+
|
|
489
|
+
## Suffix match in combined tree
|
|
490
|
+
|
|
491
|
+
* Result: suffix_50000
|
|
492
|
+
* Number of routes: 300000
|
|
493
|
+
* Number of matches: 100000
|
|
494
|
+
* Execution time: 525 ms
|
|
495
|
+
* QPS: 190578
|
|
496
|
+
|
|
497
|
+
## Route not found in combined tree
|
|
498
|
+
|
|
499
|
+
* Result: null
|
|
500
|
+
* Number of routes: 300000
|
|
501
|
+
* Number of matches: 100000
|
|
502
|
+
* Execution time: 444 ms
|
|
503
|
+
* QPS: 225436
|
|
504
|
+
|
|
505
|
+
## Match hosts
|
|
506
|
+
|
|
507
|
+
* Result: hosts_500
|
|
508
|
+
* Number of routes: 1000
|
|
509
|
+
* Number of matches: 100000
|
|
510
|
+
* Execution time: 3067 ms
|
|
511
|
+
* QPS: 32608
|
|
512
|
+
|
|
513
|
+
## Match wildcard hosts
|
|
514
|
+
|
|
515
|
+
* Result: wildcard_hosts_500
|
|
516
|
+
* Number of routes: 1000
|
|
517
|
+
* Number of matches: 100000
|
|
518
|
+
* Execution time: 3995 ms
|
|
519
|
+
* QPS: 25029
|
|
520
|
+
|
|
521
|
+
## Match remote address, route found
|
|
522
|
+
|
|
523
|
+
* Result: { access: 'local' }
|
|
524
|
+
* Number of routes: 1000
|
|
525
|
+
* Number of matches: 1000
|
|
526
|
+
* Execution time: 172 ms
|
|
527
|
+
* QPS: 5804
|
|
528
|
+
|
|
529
|
+
## Match remote address, route not found
|
|
530
|
+
|
|
531
|
+
* Result: { access: 'public' }
|
|
532
|
+
* Number of routes: 1000
|
|
533
|
+
* Number of matches: 1000
|
|
534
|
+
* Execution time: 300 ms
|
|
535
|
+
* QPS: 3327
|
|
536
|
+
|
|
537
|
+
## Match expressions and headers, route found
|
|
538
|
+
|
|
539
|
+
* Result: { version: 'v501' }
|
|
540
|
+
* Number of routes: 1000
|
|
541
|
+
* Number of matches: 1000
|
|
542
|
+
* Execution time: 442 ms
|
|
543
|
+
* QPS: 2262
|
|
544
|
+
|
|
545
|
+
## Match expressions and headers, route not found
|
|
546
|
+
|
|
547
|
+
* Result: { version: 'default' }
|
|
548
|
+
* Number of routes: 1000
|
|
549
|
+
* Number of matches: 1000
|
|
550
|
+
* Execution time: 778 ms
|
|
551
|
+
* QPS: 1285
|
|
552
|
+
|
|
553
|
+
## Match expressions and parameters, route found
|
|
554
|
+
|
|
555
|
+
* Result: { discount: '10%' }
|
|
556
|
+
* Number of routes: 1000
|
|
557
|
+
* Number of matches: 1000
|
|
558
|
+
* Execution time: 275 ms
|
|
559
|
+
* QPS: 3632
|
|
560
|
+
|
|
561
|
+
## Match expressions and parameters, route not found
|
|
562
|
+
|
|
563
|
+
* Result: { discount: '0%' }
|
|
564
|
+
* Number of routes: 1000
|
|
565
|
+
* Number of matches: 1000
|
|
566
|
+
* Execution time: 475 ms
|
|
567
|
+
* QPS: 2104
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
[Back to TOC](#table-of-contents)
|
|
571
|
+
|
|
572
|
+
# License
|
|
573
|
+
|
|
574
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
const { benchmark, md5 } = require('./utility');
|
|
3
|
+
const routeCount = 1000 * 100;
|
|
4
|
+
const matchTimes = 1000 * 1000 * 10;
|
|
5
|
+
|
|
6
|
+
const routes = [];
|
|
7
|
+
for (let i = 1; i <= routeCount; i++) {
|
|
8
|
+
routes.push({
|
|
9
|
+
paths: ["/" + md5(i.toString())],
|
|
10
|
+
meta: `equals_${i}`
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const middleValue = Math.floor(routeCount / 2).toString();
|
|
15
|
+
const path = "/" + md5(middleValue);
|
|
16
|
+
|
|
17
|
+
const req = {
|
|
18
|
+
path: path,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
benchmark("Match equals", routeCount, matchTimes, routes, req);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
const { benchmark, md5 } = require('./utility');
|
|
3
|
+
const routeCount = 1000 * 100;
|
|
4
|
+
const matchTimes = 1000 * 1000;
|
|
5
|
+
const routes = [];
|
|
6
|
+
for (let i = 1; i <= routeCount; i++) {
|
|
7
|
+
routes.push({
|
|
8
|
+
paths: ["**." + md5(i.toString())],
|
|
9
|
+
meta: `suffix_${i}`
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const middleValue = Math.floor(routeCount / 2).toString();
|
|
14
|
+
const path = "abc." + md5(middleValue);
|
|
15
|
+
|
|
16
|
+
const req = {
|
|
17
|
+
path: path,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
benchmark("Match suffix", routeCount, matchTimes, routes, req);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
const { benchmark, md5 } = require('./utility');
|
|
3
|
+
const routeCount = 1000 * 100;
|
|
4
|
+
const matchTimes = 1000 * 1000;
|
|
5
|
+
const routes = [];
|
|
6
|
+
for (let i = 1; i <= routeCount; i++) {
|
|
7
|
+
routes.push({
|
|
8
|
+
paths: ["/" + md5(i.toString()) + "/*"],
|
|
9
|
+
meta: `prefix_${i}`
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const middleValue = Math.floor(routeCount / 2).toString();
|
|
14
|
+
const path = "/" + md5(middleValue) + "/a";
|
|
15
|
+
|
|
16
|
+
const req = {
|
|
17
|
+
path: path,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
benchmark("Match prefix, route found", routeCount, matchTimes, routes, req);
|
|
21
|
+
|
|
22
|
+
const path2 = "/" + md5((routeCount + 1).toString()) + "/a";
|
|
23
|
+
|
|
24
|
+
const req2 = {
|
|
25
|
+
path: path2,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
benchmark("Match prefix, route not found", routeCount, matchTimes, routes, req2);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { benchmark, md5 } = require('./utility');
|
|
2
|
+
const routeCount = 1000 * 100;
|
|
3
|
+
const matchTimes = 1000 * 100;
|
|
4
|
+
|
|
5
|
+
const routes = [];
|
|
6
|
+
for (let i = 1; i <= routeCount; i++) {
|
|
7
|
+
const hash = md5(i.toString());
|
|
8
|
+
routes.push(
|
|
9
|
+
{ paths: ["/" + hash], meta: `equals_${i}` },
|
|
10
|
+
{ paths: ["/" + hash + "/*"], meta: `prefix_${i}` },
|
|
11
|
+
{ paths: ["**." + hash], meta: `suffix_${i}` }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Calculate middle value
|
|
16
|
+
const middleValue = Math.floor(routeCount / 2).toString();
|
|
17
|
+
|
|
18
|
+
// Equal match test in combined tree
|
|
19
|
+
const equalPath = "/" + md5(middleValue);
|
|
20
|
+
const equalReq = { path: equalPath };
|
|
21
|
+
benchmark("Equal match in combined tree", routeCount * 3, matchTimes, routes, equalReq);
|
|
22
|
+
|
|
23
|
+
// Prefix match test in combined tree
|
|
24
|
+
const prefixPath = "/" + md5(middleValue) + "/a";
|
|
25
|
+
const prefixReq = { path: prefixPath };
|
|
26
|
+
benchmark("Prefix match in combined tree", routeCount * 3, matchTimes, routes, prefixReq);
|
|
27
|
+
|
|
28
|
+
// Suffix match test in combined tree
|
|
29
|
+
const suffixPath = "abc." + md5(middleValue);
|
|
30
|
+
const suffixReq = { path: suffixPath };
|
|
31
|
+
benchmark("Suffix match in combined tree", routeCount * 3, matchTimes, routes, suffixReq);
|
|
32
|
+
|
|
33
|
+
// Route not found test in combined tree
|
|
34
|
+
const notFoundPath = "/" + md5((routeCount + 1).toString()) + "/notfound";
|
|
35
|
+
const notFoundReq = { path: notFoundPath };
|
|
36
|
+
benchmark("Route not found in combined tree", routeCount * 3, matchTimes, routes, notFoundReq);
|