extreme-router 1.0.0
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/CHANGELOG.MD +50 -0
- package/LICENSE +21 -0
- package/README.md +648 -0
- package/assets/extreme-router-logo.png +0 -0
- package/dist/bundle-size.json +22 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +314 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/docs/error-types.md +35 -0
- package/docs/examples/server.bun.md +73 -0
- package/docs/examples/server.deno.md +71 -0
- package/docs/examples/server.node.md +102 -0
- package/docs/optional-parameters-priority.md +64 -0
- package/package.json +107 -0
package/README.md
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/extreme-router-logo.png" alt="Extreme Router Logo" width="240">
|
|
3
|
+
<h1 align="center">⚡ Extreme Router – Fast and Extensible ⚡</h1>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-orange.svg?color=orange" alt="License: MIT"></a>
|
|
8
|
+
|
|
9
|
+
🔥 **A high-performance, tree-based router for JavaScript and TypeScript, featuring a powerful plugin system for extreme extensibility.**
|
|
10
|
+
|
|
11
|
+
Extreme Router is designed for speed and flexibility. It uses an optimized radix tree (trie) structure for fast dynamic route matching and a dedicated cache for O(1) static route lookups, while its plugin architecture allows you to easily extend its capabilities to handle virtually any URL pattern.
|
|
12
|
+
|
|
13
|
+
## 📚 Table of Contents
|
|
14
|
+
|
|
15
|
+
- [✨ Features](#features)
|
|
16
|
+
- [🚀 Installation](#installation)
|
|
17
|
+
- [💡 Basic Usage](#basic-usage)
|
|
18
|
+
- [⚡ Advanced Usage](#advanced-usage)
|
|
19
|
+
- [🔌 Built-in Plugins](#built-in-plugins)
|
|
20
|
+
- [Example using regex param plugin](#example-using-regex-param-plugin)
|
|
21
|
+
- [🛠️ Custom Plugins](#custom-plugins)
|
|
22
|
+
- [⚙️ API](#api)
|
|
23
|
+
- [Error Types](./docs/error-types.md)
|
|
24
|
+
- [📊 Benchmarks](#benchmarks)
|
|
25
|
+
- [✅ Testing](#testing)
|
|
26
|
+
- [🙏 Acknowledgments](#acknowledgments)
|
|
27
|
+
- [🤝 Contributing](#contributing)
|
|
28
|
+
- [📜 License](#license)
|
|
29
|
+
|
|
30
|
+
<span id="features"></span>
|
|
31
|
+
|
|
32
|
+
## ✨ Features
|
|
33
|
+
|
|
34
|
+
- **Blazing Fast:** Optimized radix tree implementation for O(k) lookup (k = path length), with a dedicated cache for static routes (O(1)).
|
|
35
|
+
- **Universal Compatibility:** Runs seamlessly on every JavaScript environment.
|
|
36
|
+
- **Static & Dynamic Routing:** Supports fixed paths, parameterized segments, and wildcards.
|
|
37
|
+
- **Path Normalization:** Automatically normalizes paths by removing trailing slashes and collapsing multiple consecutive slashes (e.g., `/a//b///c/` becomes `/a/b/c`).
|
|
38
|
+
- **No URI Decoding by Default:** The router operates on raw path segments. URI decoding (e.g., `%20` to space) should be handled by the user before matching if needed.
|
|
39
|
+
- **Extensible Plugin System:** Easily add custom logic for complex routing patterns.
|
|
40
|
+
- **Smart Optional Parameter Handling:** Efficiently generates all unique path combinations (2^n) for routes with optional parameters using bitwise operations, ensuring comprehensive matching.
|
|
41
|
+
- **Built-in Plugins:** Comes with essential plugins for common use cases:
|
|
42
|
+
- Parameters (`:id`)
|
|
43
|
+
- Wildcards (`*`, `:name*`)
|
|
44
|
+
- Regex Parameters (`:id<\\d+>`)
|
|
45
|
+
- Optional Parameters (`:id?`)
|
|
46
|
+
- File Extension Parameters (`:file.ext`)
|
|
47
|
+
- Group Parameters (`/:paramName(val1|val2)`)
|
|
48
|
+
- Prefix Group Parameters (`img(png|jpg|gif)`)
|
|
49
|
+
- Optional Prefix Group Parameters (`img(png|jpg|gif)?`)
|
|
50
|
+
- **TypeScript Native:** Written entirely in TypeScript with excellent type support.
|
|
51
|
+
- **Zero Dependencies:** Lightweight and dependency-free core.
|
|
52
|
+
- **Compact Size:** The core library is lightweight: **12.87 KB minified** / **3.81 KB gzipped** (ESM) and **13.44 KB minified** / **4.06 KB gzipped** (CJS).
|
|
53
|
+
- **Well-Tested:** Comprehensive test suite ensuring reliability with **100% code coverage**.
|
|
54
|
+
- **Benchmarked:** Performance is continuously monitored.
|
|
55
|
+
|
|
56
|
+
<span id="installation"></span>
|
|
57
|
+
|
|
58
|
+
## 🚀 Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bun install extreme-router
|
|
62
|
+
# or
|
|
63
|
+
npm install extreme-router
|
|
64
|
+
# or
|
|
65
|
+
yarn add extreme-router
|
|
66
|
+
# or
|
|
67
|
+
pnpm add extreme-router
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
<span id="basic-usage"></span>
|
|
71
|
+
|
|
72
|
+
## 💡 Basic Usage
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import Extreme, { param, wildcard } from 'extreme-router';
|
|
76
|
+
|
|
77
|
+
// 1. Initialize the router
|
|
78
|
+
const router = new Extreme<{ handler: string }>(); // Specify the type for your route store
|
|
79
|
+
// Alternatively, specify a custom store factory function:
|
|
80
|
+
// const router = new Extreme<{ handler: string }>({ storeFactory: () => ({ handler: 'SharedHandler' }) });
|
|
81
|
+
|
|
82
|
+
// 2. Register plugins (chaining supported)
|
|
83
|
+
router.use(param).use(wildcard);
|
|
84
|
+
|
|
85
|
+
// Alternatively, you can register plugins when creating the router:
|
|
86
|
+
// const router = new Extreme<{ handler: string }>({
|
|
87
|
+
// plugins: [param, wildcard],
|
|
88
|
+
// });
|
|
89
|
+
|
|
90
|
+
// 3. Register routes
|
|
91
|
+
// The register method returns the store object associated with the route.
|
|
92
|
+
// The store object is created by the storeFactory function (if provided) or defaults to an empty object.
|
|
93
|
+
// You can use the store object to attach any data to the route, such as handler functions, HTTP methods, middlewares, or other metadata.
|
|
94
|
+
|
|
95
|
+
router.register('/').handler = 'HomePage';
|
|
96
|
+
router.register('/users').handler = 'UserListPage';
|
|
97
|
+
router.register('/users/:userId').handler = 'UserProfilePage';
|
|
98
|
+
router.register('/files/*').handler = 'FileCatchAll';
|
|
99
|
+
|
|
100
|
+
// 4. Match paths
|
|
101
|
+
const match1 = router.match('/');
|
|
102
|
+
// match1 = { handler: 'HomePage' }
|
|
103
|
+
|
|
104
|
+
const match2 = router.match('/users/123');
|
|
105
|
+
// match2 = { handler: 'UserProfilePage', params: { userId: '123' } }
|
|
106
|
+
|
|
107
|
+
const match3 = router.match('/files/a/b/c.txt');
|
|
108
|
+
// match3 = { handler: 'FileCatchAll', params: { '*': 'a/b/c.txt' } }
|
|
109
|
+
|
|
110
|
+
const match4 = router.match('/nonexistent');
|
|
111
|
+
// match4 = null
|
|
112
|
+
|
|
113
|
+
router.unregister('/users/:userId'); // Unregister a specific route
|
|
114
|
+
|
|
115
|
+
const match5 = router.match('/users/123');
|
|
116
|
+
// match5 = null // Unregistered route, no match
|
|
117
|
+
|
|
118
|
+
console.log(router.inspect());
|
|
119
|
+
/*
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
path: "/",
|
|
123
|
+
type: "static",
|
|
124
|
+
store: {
|
|
125
|
+
handler: "HomePage",
|
|
126
|
+
},
|
|
127
|
+
}, {
|
|
128
|
+
path: "/users",
|
|
129
|
+
type: "static",
|
|
130
|
+
store: {
|
|
131
|
+
handler: "UserListPage",
|
|
132
|
+
},
|
|
133
|
+
}, {
|
|
134
|
+
path: "/files/*",
|
|
135
|
+
type: "dynamic",
|
|
136
|
+
store: [Object: null prototype] {
|
|
137
|
+
handler: "FileCatchAll",
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
*/
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
<span id="advanced-usage"></span>
|
|
145
|
+
|
|
146
|
+
## ⚡ Advanced Usage
|
|
147
|
+
|
|
148
|
+
Here are examples `docs/examples` of how to integrate Extreme Router into simple HTTP servers using different JavaScript runtimes.
|
|
149
|
+
|
|
150
|
+
- [Bun HTTP Server](./docs/examples/server.bun.md)
|
|
151
|
+
- [Node.js HTTP Server](./docs/examples/server.node.md)
|
|
152
|
+
- [Deno HTTP Server](./docs/examples/server.deno.md)
|
|
153
|
+
|
|
154
|
+
## 🔌 Built-in Plugins
|
|
155
|
+
|
|
156
|
+
Extreme Router comes with several pre-built plugins. You need to register them using `router.use()` before registering routes that depend on them. When matching a URL segment against potential dynamic routes, the router checks the registered plugins based on their **`priority`** value. **Lower priority numbers are checked first**.
|
|
157
|
+
|
|
158
|
+
| Priority | Plugin | Syntax Example | Description | Example Usage (after registering plugin) |
|
|
159
|
+
| :------- | :-------------------- | :-------------------- | :---------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
160
|
+
| 100 | `prefixGroup` | `/img(png\|jpg\|gif)` | Matches a static prefix followed by one of a predefined set. | `router.register('/img(png\|jpg)');` <br> `match('/imgpng'); // Match` <br> `match('/img'); // No Match` |
|
|
161
|
+
| 200 | `optionalPrefixGroup` | `/img(png\|jpg)?` | Matches a static prefix optionally followed by one of a predefined set. | `router.register('/img(png\|jpg)?');` <br> `match('/imgpng'); // Match` <br> `match('/img'); // Match` |
|
|
162
|
+
| 300 | `groupParam` | `/:png(\|jpg\|gif)` | Matches one of a predefined set of static values as a parameter. | `router.register('/:fmt(png\|jpg)');` <br> `match('/png'); // { params: { fmt: 'png' } }` <br> `match('/gif'); // No Match` |
|
|
163
|
+
| 400 | `regexParam` | `/:id<\\d+>` | Matches a named parameter against a custom regex. | `router.register('/user/:id<\\d+>');` <br> `match('/user/123'); // { params: { id: '123' } }` <br> `match('/user/abc'); // No Match` |
|
|
164
|
+
| 500 | `extensionParam` | `/:file.ext` | Matches segments with a specific file extension. | `router.register('/:file.:ext');` <br> `match('/report.pdf'); // { params: { file: 'report', ext: 'pdf' } }` |
|
|
165
|
+
| 600 | `optionalParam` | `/:id?` | Matches an optional named parameter. See note below on priority. | `router.register('/product/:id?');` <br> `match('/product/123'); // { params: { id: '123' } }` <br> `match('/product'); // Match (no params)` |
|
|
166
|
+
| 700 | `param` | `/:id` | Matches a standard named parameter. | `router.register('/post/:slug');` <br> `match('/post/hello'); // { params: { slug: 'hello' } }` |
|
|
167
|
+
| 800 | `wildcard` | `/*`, `/:name*` | Matches the rest of the path. Must be the last segment. | `router.register('/files/*');` <br> `match('/files/a/b'); // { params: { '*': 'a/b' } }` <br> `router.register('/docs/:p*'); // { params: { p: ... } }` |
|
|
168
|
+
|
|
169
|
+
<span id="note-on-optional-parameters-and-priority"></span>
|
|
170
|
+
[See Note on Optional Parameters and Priority](./docs/optional-parameters-priority.md)
|
|
171
|
+
|
|
172
|
+
<span id="example-using-multiple-plugins"></span>
|
|
173
|
+
|
|
174
|
+
### Example using regex param plugin
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import Extreme, { regexParam } from 'extreme-router';
|
|
178
|
+
|
|
179
|
+
// Initialize the router
|
|
180
|
+
const router = new Extreme<{ handler: string }>();
|
|
181
|
+
|
|
182
|
+
// Register plugins
|
|
183
|
+
router.use(regexParam);
|
|
184
|
+
|
|
185
|
+
// Register route
|
|
186
|
+
router.register('/users/:userId<\\d+>').handler = 'UserProfilePage';
|
|
187
|
+
|
|
188
|
+
// Match paths
|
|
189
|
+
const match1 = router.match('/users/123');
|
|
190
|
+
// match1 = { handler: 'UserProfilePage', params: { userId: '123' } }
|
|
191
|
+
|
|
192
|
+
const match2 = router.match('/users/abc');
|
|
193
|
+
// match2 = null // No match, regex didn't match
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
<span id="custom-plugins"></span>
|
|
197
|
+
|
|
198
|
+
## 🛠️ Custom Plugins
|
|
199
|
+
|
|
200
|
+
Extreme Router's power lies in its extensibility. You can easily create your own plugins to handle unique URL patterns or add custom matching logic. The process involves defining a plugin function that returns a configuration object, which in turn includes a handler function responsible for recognizing syntax and providing the runtime matching logic.
|
|
201
|
+
|
|
202
|
+
**Core Types:** (from [`src/types.ts`](c:\Users\lior3\Development\liodex\extreme-router\src\types.ts))
|
|
203
|
+
|
|
204
|
+
1. **`Plugin`**: `() => PluginConfig`
|
|
205
|
+
|
|
206
|
+
- The function you register with `router.use()`. It's a factory function that, when called, returns a `PluginConfig` object. This allows plugins to be configured or initialized if needed, though simple plugins might just return a static configuration object.
|
|
207
|
+
|
|
208
|
+
2. **`PluginConfig`**: `{ id: string, priority: number, syntax: string, handler: PluginHandler }`
|
|
209
|
+
|
|
210
|
+
- Defines the plugin's identity, precedence, the **representative syntax pattern** it handles, and the handler function.
|
|
211
|
+
- **`id: string`**: A unique identifier for the plugin (e.g., `"param"`, `"myCustomPlugin"`). This is used internally and for error reporting.
|
|
212
|
+
- **`priority: number`**: A number determining the order in which plugins are evaluated during route registration and matching. Lower numbers have higher priority. Built-in plugins have priorities like `param` (700) and `wildcard` (800). Choose a priority that makes sense relative to other plugins.
|
|
213
|
+
- **`syntax: string`**: A representative string example of the syntax this plugin handles (e.g., `":paramName"`, `":id<regex>"`, `"*"`). This string is passed to the `plugin.handler` during `router.use()` to validate that the handler can correctly process this type of syntax.
|
|
214
|
+
- **`handler: PluginHandler`**: The function responsible for processing path segments during route registration.
|
|
215
|
+
|
|
216
|
+
3. **`PluginHandler`**: `(segment: string) => PluginMeta | undefined | null`
|
|
217
|
+
|
|
218
|
+
- Called during `router.register()`. It receives a path segment string (e.g., `":userId"`, `":id<uuid>"`, `"*"`).
|
|
219
|
+
- Its job is to determine if this `segment` matches the pattern the plugin is designed for.
|
|
220
|
+
- If it matches, it should return a `PluginMeta` object containing the necessary information for matching and parameter extraction.
|
|
221
|
+
- If it doesn't match the plugin's expected syntax, it should return `null` or `undefined` to allow other plugins to attempt to handle the segment.
|
|
222
|
+
|
|
223
|
+
4. **`PluginMeta`**: `{ paramName: string, match: (args) => boolean, override?: boolean, wildcard?: boolean, additionalMeta?: object }`
|
|
224
|
+
- Returned by the `PluginHandler` if a segment's syntax is recognized. This object is stored in the routing tree node.
|
|
225
|
+
- **`paramName: string`**: The name to be used for the parameter if the segment is dynamic (e.g., for `":userId"`, `paramName` would be `"userId"`). For non-capturing plugins (like a static prefix group), this might be an empty string.
|
|
226
|
+
- **`match: ({ urlSegment: string, urlSegments: string[], index: number, params: Record<string, unknown> }) => boolean`**: This is the crucial function called during `router.match()`.
|
|
227
|
+
- It receives the current URL segment (`urlSegment`), all URL segments (`urlSegments`), the current segment's `index`, and the `params` object (to populate if a match occurs).
|
|
228
|
+
- It must return `true` if the `urlSegment` matches the plugin's logic, and `false` otherwise.
|
|
229
|
+
- If it returns `true`, it should also populate the `params` object with any captured values.
|
|
230
|
+
- **`override?: boolean`** (optional): If `true`, this plugin can override an existing dynamic segment registered by a plugin with the _same ID_ at the same node. This is useful for plugins like `optionalParam` that might need to "claim" a segment that could also be interpreted by the base `param` plugin if the optional marker wasn't present. Defaults to `false`.
|
|
231
|
+
- **`wildcard?: boolean`** (optional): If `true`, indicates this plugin handles a wildcard match (like `*` or `:name*`). Wildcard routes have special handling (e.g., they must be at the end of a path, and matching can consume multiple remaining segments). Defaults to `false`.
|
|
232
|
+
- **`additionalMeta?: object`** (optional for logging purpose): An object to store any other metadata about the plugin's behavior. For example, the `regexParam` plugin stores the compiled `RegExp` object here.
|
|
233
|
+
- `group?: Record<string | number, unknown>`: Used by group-based plugins.
|
|
234
|
+
- `regex?: RegExp`: Used by regex-based plugins.
|
|
235
|
+
- `extension?: string`: Used by extension-based plugins.
|
|
236
|
+
|
|
237
|
+
**Example: Custom UUID Plugin**
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import Extreme, { param } from 'extreme-router';
|
|
241
|
+
import type { Plugin, PluginHandler, PluginMeta } from 'extreme-router'; // Import types
|
|
242
|
+
|
|
243
|
+
// Define the UUID regex
|
|
244
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
245
|
+
|
|
246
|
+
// 1. Define the Plugin Function
|
|
247
|
+
const uuidPlugin: Plugin = () => {
|
|
248
|
+
// 2. Define the Plugin Handler
|
|
249
|
+
const handler: PluginHandler = (segment) => {
|
|
250
|
+
// Check if the registration segment matches our syntax :name<uuid>
|
|
251
|
+
const syntaxMatch = /^:(?<paramName>[a-zA-Z0-9_-]+)<uuid>$/i.exec(segment);
|
|
252
|
+
|
|
253
|
+
if (!syntaxMatch?.groups?.paramName) {
|
|
254
|
+
return null; // Doesn't match our syntax, let other plugins handle it
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const paramName = syntaxMatch.groups.paramName;
|
|
258
|
+
|
|
259
|
+
// 3. Return the PluginMeta object
|
|
260
|
+
const meta: PluginMeta = {
|
|
261
|
+
paramName: paramName,
|
|
262
|
+
// 4. Define the runtime 'match' function
|
|
263
|
+
match: ({ urlSegment, params }) => {
|
|
264
|
+
// Check if the actual URL segment matches the UUID regex
|
|
265
|
+
if (UUID_REGEX.test(urlSegment)) {
|
|
266
|
+
params[paramName] = urlSegment; // Capture the value
|
|
267
|
+
return true; // It's a match!
|
|
268
|
+
}
|
|
269
|
+
return false; // Not a match
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
return meta;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// 5. Return the PluginConfig
|
|
276
|
+
return {
|
|
277
|
+
id: 'uuid', // Unique ID for this plugin
|
|
278
|
+
priority: 550, // Example: Higher precedence than 'param' (700)
|
|
279
|
+
syntax: ':name<uuid>', // Representative syntax pattern for validation
|
|
280
|
+
handler: handler,
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// --- Usage ---
|
|
285
|
+
const router = new Extreme<{ handler: string }>();
|
|
286
|
+
|
|
287
|
+
// Register plugins - priority determines order handlers are checked during registration
|
|
288
|
+
router
|
|
289
|
+
.use(uuidPlugin) // Priority 550
|
|
290
|
+
.use(param); // Priority 700
|
|
291
|
+
|
|
292
|
+
// Register routes: The highest-priority plugin whose handler recognizes
|
|
293
|
+
// the segment's syntax during registration determines which PluginMeta
|
|
294
|
+
// is associated with the resulting node in the routing tree.
|
|
295
|
+
router.register('/orders/:orderId<uuid>').handler = 'GetOrder'; // Handled by uuidPlugin
|
|
296
|
+
router.register('/users/:userId').handler = 'GetUser'; // Handled by param plugin
|
|
297
|
+
|
|
298
|
+
// Match paths
|
|
299
|
+
const match1 = router.match('/orders/123e4567-e89b-12d3-a456-426614174000');
|
|
300
|
+
// match1 = { handler: 'GetOrder', params: { orderId: '...' } }
|
|
301
|
+
// Uses the match function from the uuidPlugin's PluginMeta.
|
|
302
|
+
|
|
303
|
+
const match2 = router.match('/orders/invalid-uuid-format');
|
|
304
|
+
// match2 = null
|
|
305
|
+
// The uuidPlugin's match function returned false. No other dynamic nodes
|
|
306
|
+
// were registered at this specific point for '/orders/...'
|
|
307
|
+
|
|
308
|
+
const match3 = router.match('/users/regular-id');
|
|
309
|
+
// match3 = { handler: 'GetUser', params: { userId: 'regular-id' } }
|
|
310
|
+
// Uses the match function from the param plugin's PluginMeta.
|
|
311
|
+
|
|
312
|
+
console.log(match1);
|
|
313
|
+
console.log(match2);
|
|
314
|
+
console.log(match3);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
<span id="api"></span>
|
|
318
|
+
|
|
319
|
+
## ⚙️ API
|
|
320
|
+
|
|
321
|
+
- **`new Extreme<T>(options?: Options<T>)`**: Creates a new router instance.
|
|
322
|
+
- `options.storeFactory`: A function that returns a new store object for each registered route. Defaults to `() => Object.create(null)`.
|
|
323
|
+
- `options.plugins`: An array of plugin functions (`Plugin[]`) to register automatically when the router is created. Defaults to `[]`. Plugins will be applied (and sorted by priority) before any manual `router.use()` calls.
|
|
324
|
+
- **`router.use(plugin: Plugin): this`**: Registers a plugin function and returns the router instance, allowing method chaining.
|
|
325
|
+
- Example:
|
|
326
|
+
```typescript
|
|
327
|
+
router.use(param).use(wildcard).use(regexParam);
|
|
328
|
+
```
|
|
329
|
+
- **`router.register(path: string): T`**: Registers a route path and returns the associated store object (created by `storeFactory`). Throws errors for invalid paths or conflicts.
|
|
330
|
+
- **`router.unregister(path: string): boolean`**: Unregisters a route path. Returns `true` if the path was successfully unregistered, `false` otherwise.
|
|
331
|
+
- Handles static paths, dynamic paths, and paths with optional parameters.
|
|
332
|
+
- For paths with optional parameters, all generated combinations are unregistered **only if you unregister the full registered URL with the optionals**. If you unregister just one of its generated combinations, only that specific combination is removed.
|
|
333
|
+
- **`router.match(path: string): Match<T> | null`**: Matches a given path against the registered routes.
|
|
334
|
+
- Returns a `Match<T>` object if a matching route is found. `Match<T>` is the route's store `T` augmented with a `params: Record<string, string>` property.
|
|
335
|
+
- For dynamic path matches, the returned object includes a `params` property containing the extracted parameter values.
|
|
336
|
+
- For static path matches, the returned object is simply the route's store. While the `Match<T>` type includes a `params` property, it will not be present as an own property on the returned store object.
|
|
337
|
+
- Returns `null` if no match is found.
|
|
338
|
+
- **`router.inspect(): ListedRoute<T>[]`**: Retrieves a list of all registered routes. This is useful for debugging or administrative purposes.
|
|
339
|
+
- Returns an array of `ListedRoute<T>` objects. Each object has the following properties:
|
|
340
|
+
- `path: string`: The registered path string.
|
|
341
|
+
- `type: 'static' | 'dynamic'`: The type of the route.
|
|
342
|
+
- `store: T`: The original store object associated with the route.
|
|
343
|
+
- **Error Handling**: The router uses a set of predefined [Error Types](./docs/error-types.md) for consistent error reporting.
|
|
344
|
+
|
|
345
|
+
<span id="benchmarks"></span>
|
|
346
|
+
|
|
347
|
+
## 📊 Benchmarks
|
|
348
|
+
|
|
349
|
+
The following benchmarks measure the raw speed of the `router.match()` operation (ops/sec) for different route types and route counts.
|
|
350
|
+
|
|
351
|
+
Benchmarks were conducted on: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz, 16GB RAM.
|
|
352
|
+
|
|
353
|
+
### Benchmark Results
|
|
354
|
+
|
|
355
|
+
**Matching Benchmarks (ops/sec)**
|
|
356
|
+
Higher is better.
|
|
357
|
+
|
|
358
|
+
<table style="width:100%; border: none; border-spacing: 0;">
|
|
359
|
+
<tr style="border: none;">
|
|
360
|
+
<td style="width:50%; vertical-align:top; padding-right:10px; padding-bottom:15px; border: none;">
|
|
361
|
+
<strong>25 Routes</strong>
|
|
362
|
+
<table>
|
|
363
|
+
<thead>
|
|
364
|
+
<tr>
|
|
365
|
+
<th>Runtime</th>
|
|
366
|
+
<th>Type</th>
|
|
367
|
+
<th>Ops/sec</th>
|
|
368
|
+
</tr>
|
|
369
|
+
</thead>
|
|
370
|
+
<tbody>
|
|
371
|
+
<tr>
|
|
372
|
+
<td>Bun</td>
|
|
373
|
+
<td>Static</td>
|
|
374
|
+
<td>40,664,369.57</td>
|
|
375
|
+
</tr>
|
|
376
|
+
<tr>
|
|
377
|
+
<td>Node</td>
|
|
378
|
+
<td>Static</td>
|
|
379
|
+
<td>32,699,587.31</td>
|
|
380
|
+
</tr>
|
|
381
|
+
<tr>
|
|
382
|
+
<td>Bun</td>
|
|
383
|
+
<td>Mixed</td>
|
|
384
|
+
<td>11,275,739.98</td>
|
|
385
|
+
</tr>
|
|
386
|
+
<tr>
|
|
387
|
+
<td>Node</td>
|
|
388
|
+
<td>Mixed</td>
|
|
389
|
+
<td>7,984,073.83</td>
|
|
390
|
+
</tr>
|
|
391
|
+
<tr>
|
|
392
|
+
<td>Bun</td>
|
|
393
|
+
<td>Dynamic</td>
|
|
394
|
+
<td>5,315,190.38</td>
|
|
395
|
+
</tr>
|
|
396
|
+
<tr>
|
|
397
|
+
<td>Node</td>
|
|
398
|
+
<td>Dynamic</td>
|
|
399
|
+
<td>3,700,454.56</td>
|
|
400
|
+
</tr>
|
|
401
|
+
</tbody>
|
|
402
|
+
</table>
|
|
403
|
+
</td>
|
|
404
|
+
<td style="width:50%; vertical-align:top; padding-left:10px; padding-bottom:15px; border: none;">
|
|
405
|
+
<strong>100 Routes</strong>
|
|
406
|
+
<table>
|
|
407
|
+
<thead>
|
|
408
|
+
<tr>
|
|
409
|
+
<th>Runtime</th>
|
|
410
|
+
<th>Type</th>
|
|
411
|
+
<th>Ops/sec</th>
|
|
412
|
+
</tr>
|
|
413
|
+
</thead>
|
|
414
|
+
<tbody>
|
|
415
|
+
<tr>
|
|
416
|
+
<td>Bun</td>
|
|
417
|
+
<td>Static</td>
|
|
418
|
+
<td>43,161,335.67</td>
|
|
419
|
+
</tr>
|
|
420
|
+
<tr>
|
|
421
|
+
<td>Node</td>
|
|
422
|
+
<td>Static</td>
|
|
423
|
+
<td>30,731,126.72</td>
|
|
424
|
+
</tr>
|
|
425
|
+
<tr>
|
|
426
|
+
<td>Bun</td>
|
|
427
|
+
<td>Mixed</td>
|
|
428
|
+
<td>10,314,999.21</td>
|
|
429
|
+
</tr>
|
|
430
|
+
<tr>
|
|
431
|
+
<td>Node</td>
|
|
432
|
+
<td>Mixed</td>
|
|
433
|
+
<td>7,047,826.06</td>
|
|
434
|
+
</tr>
|
|
435
|
+
<tr>
|
|
436
|
+
<td>Bun</td>
|
|
437
|
+
<td>Dynamic</td>
|
|
438
|
+
<td>2,570,193.17</td>
|
|
439
|
+
</tr>
|
|
440
|
+
<tr>
|
|
441
|
+
<td>Node</td>
|
|
442
|
+
<td>Dynamic</td>
|
|
443
|
+
<td>1,791,611.34</td>
|
|
444
|
+
</tr>
|
|
445
|
+
</tbody>
|
|
446
|
+
</table>
|
|
447
|
+
</td>
|
|
448
|
+
</tr>
|
|
449
|
+
<tr style="border: none;">
|
|
450
|
+
<td style="width:50%; vertical-align:top; padding-right:10px; border: none;">
|
|
451
|
+
<strong>500 Routes</strong>
|
|
452
|
+
<table>
|
|
453
|
+
<thead>
|
|
454
|
+
<tr>
|
|
455
|
+
<th>Runtime</th>
|
|
456
|
+
<th>Type</th>
|
|
457
|
+
<th>Ops/sec</th>
|
|
458
|
+
</tr>
|
|
459
|
+
</thead>
|
|
460
|
+
<tbody>
|
|
461
|
+
<tr>
|
|
462
|
+
<td>Bun</td>
|
|
463
|
+
<td>Static</td>
|
|
464
|
+
<td>30,417,507.26</td>
|
|
465
|
+
</tr>
|
|
466
|
+
<tr>
|
|
467
|
+
<td>Node</td>
|
|
468
|
+
<td>Static</td>
|
|
469
|
+
<td>28,521,879.69</td>
|
|
470
|
+
</tr>
|
|
471
|
+
<tr>
|
|
472
|
+
<td>Bun</td>
|
|
473
|
+
<td>Mixed</td>
|
|
474
|
+
<td>5,597,866.27</td>
|
|
475
|
+
</tr>
|
|
476
|
+
<tr>
|
|
477
|
+
<td>Node</td>
|
|
478
|
+
<td>Mixed</td>
|
|
479
|
+
<td>4,139,942.82</td>
|
|
480
|
+
</tr>
|
|
481
|
+
<tr>
|
|
482
|
+
<td>Bun</td>
|
|
483
|
+
<td>Dynamic</td>
|
|
484
|
+
<td>1,822,528.37</td>
|
|
485
|
+
</tr>
|
|
486
|
+
<tr>
|
|
487
|
+
<td>Node</td>
|
|
488
|
+
<td>Dynamic</td>
|
|
489
|
+
<td>1,226,324.41</td>
|
|
490
|
+
</tr>
|
|
491
|
+
</tbody>
|
|
492
|
+
</table>
|
|
493
|
+
</td>
|
|
494
|
+
<td style="width:50%; vertical-align:top; padding-left:10px; border: none;">
|
|
495
|
+
<strong>1000 Routes</strong>
|
|
496
|
+
<table>
|
|
497
|
+
<thead>
|
|
498
|
+
<tr>
|
|
499
|
+
<th>Runtime</th>
|
|
500
|
+
<th>Type</th>
|
|
501
|
+
<th>Ops/sec</th>
|
|
502
|
+
</tr>
|
|
503
|
+
</thead>
|
|
504
|
+
<tbody>
|
|
505
|
+
<tr>
|
|
506
|
+
<td>Bun</td>
|
|
507
|
+
<td>Static</td>
|
|
508
|
+
<td>25,570,061.69</td>
|
|
509
|
+
</tr>
|
|
510
|
+
<tr>
|
|
511
|
+
<td>Node</td>
|
|
512
|
+
<td>Static</td>
|
|
513
|
+
<td>27,940,237.55</td>
|
|
514
|
+
</tr>
|
|
515
|
+
<tr>
|
|
516
|
+
<td>Bun</td>
|
|
517
|
+
<td>Mixed</td>
|
|
518
|
+
<td>4,723,668.94</td>
|
|
519
|
+
</tr>
|
|
520
|
+
<tr>
|
|
521
|
+
<td>Node</td>
|
|
522
|
+
<td>Mixed</td>
|
|
523
|
+
<td>3,477,167.42</td>
|
|
524
|
+
</tr>
|
|
525
|
+
<tr>
|
|
526
|
+
<td>Bun</td>
|
|
527
|
+
<td>Dynamic</td>
|
|
528
|
+
<td>1,859,733.12</td>
|
|
529
|
+
</tr>
|
|
530
|
+
<tr>
|
|
531
|
+
<td>Node</td>
|
|
532
|
+
<td>Dynamic</td>
|
|
533
|
+
<td>1,166,799.26</td>
|
|
534
|
+
</tr>
|
|
535
|
+
</tbody>
|
|
536
|
+
</table>
|
|
537
|
+
</td>
|
|
538
|
+
</tr>
|
|
539
|
+
</table>
|
|
540
|
+
|
|
541
|
+
#### Stress Test Benchmarks
|
|
542
|
+
|
|
543
|
+
Total matches performed in 20 seconds with 50 concurrent workers. Higher is better.
|
|
544
|
+
|
|
545
|
+
| Runtime | Routes | Total Matches |
|
|
546
|
+
| :------ | :----- | :------------ |
|
|
547
|
+
| Bun | 25 | 151,799,882 |
|
|
548
|
+
| Node | 25 | 92,383,913 |
|
|
549
|
+
| Bun | 100 | 129,399,072 |
|
|
550
|
+
| Node | 100 | 78,502,959 |
|
|
551
|
+
| Bun | 500 | 75,988,452 |
|
|
552
|
+
| Node | 500 | 50,230,329 |
|
|
553
|
+
| Bun | 1000 | 66,190,291 |
|
|
554
|
+
| Node | 1000 | 46,227,299 |
|
|
555
|
+
|
|
556
|
+
#### Memory Usage Benchmarks
|
|
557
|
+
|
|
558
|
+
Test duration: 30 seconds. Lower heap usage and increase is generally better.
|
|
559
|
+
|
|
560
|
+
| Runtime | Routes | Start Heap | Stable End Heap | Peak Heap | Increase (Stable End - Start) |
|
|
561
|
+
| :------ | :----- | :--------- | :-------------- | :-------- | :---------------------------- |
|
|
562
|
+
| Bun | 25 | 228.86 KB | 1.97 MB | 2.04 MB | 1.75 MB (782.49%) |
|
|
563
|
+
| Node | 25 | 5.33 MB | 6.53 MB | 8.56 MB | 1.19 MB (22.39%) |
|
|
564
|
+
| Bun | 100 | 228.86 KB | 2.06 MB | 2.15 MB | 1.83 MB (820.44%) |
|
|
565
|
+
| Node | 100 | 5.47 MB | 6.7 MB | 8.63 MB | 1.23 MB (22.58%) |
|
|
566
|
+
| Bun | 500 | 228.86 KB | 2.18 MB | 2.27 MB | 1.96 MB (876.51%) |
|
|
567
|
+
| Node | 500 | 5.67 MB | 7.83 MB | 12.04 MB | 2.15 MB (37.99%) |
|
|
568
|
+
| Bun | 1000 | 228.86 KB | 2.37 MB | 2.45 MB | 2.15 MB (961.21%) |
|
|
569
|
+
| Node | 1000 | 5.96 MB | 9.02 MB | 12.12 MB | 3.06 MB (51.26%) |
|
|
570
|
+
|
|
571
|
+
### Understanding Bun vs. Node.js Memory Behavior
|
|
572
|
+
|
|
573
|
+
The memory benchmark results highlight differing memory usage patterns between Bun and Node.js. These differences primarily stem from their underlying JavaScript engines and memory management strategies:
|
|
574
|
+
|
|
575
|
+
1. **JavaScript Engines:**
|
|
576
|
+
|
|
577
|
+
- **Bun:** Utilizes JavaScriptCore (JSC), known for quick startup and potentially lower initial memory consumption.
|
|
578
|
+
- **Node.js:** Employs V8, which is highly optimized for long-running server applications.
|
|
579
|
+
|
|
580
|
+
2. **Initial Heap Size and Growth:**
|
|
581
|
+
|
|
582
|
+
- **Bun (JSC):** The benchmarks show Bun starting with a very small heap (e.g., `228.86 KB`). This results in a large _percentage_ increase as the application allocates memory, even if the final _absolute_ heap size remains relatively small (around 2 MB).
|
|
583
|
+
- **Node.js (V8):** Node.js starts with a considerably larger initial heap (e.g., `5.33 MB - 5.67 MB`). Consequently, its _percentage_ increase is smaller for comparable absolute memory growth.
|
|
584
|
+
|
|
585
|
+
3. **Interpreting the "Increase":**
|
|
586
|
+
- The significant _percentage_ increase in Bun's memory usage is largely due to its low starting base. The "Stable End Heap" and absolute MB increase offer a clearer view of the memory actively used during the test.
|
|
587
|
+
- Both runtimes demonstrate memory stability under the test conditions, suggesting `extreme-router` itself is not exhibiting a runaway memory leak. The observed variations are more indicative of the engines' default heap management behaviors.
|
|
588
|
+
|
|
589
|
+
In essence, Bun/JSC's strategy leads to a low initial memory footprint, causing high percentage growth to a still modest absolute size. Node/V8 begins with a larger heap, resulting in smaller percentage growth for similar absolute increases. Both appear to manage memory effectively for the router in these tests.
|
|
590
|
+
|
|
591
|
+
You can run benchmarks to see Extreme Router's performance:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
# Matching benchmark (25 routes by default)
|
|
595
|
+
# General mixed benchmark
|
|
596
|
+
bun run benchmark # static and dynamic routes
|
|
597
|
+
# Specify type: static, dynamic
|
|
598
|
+
bun run benchmark:static
|
|
599
|
+
bun run benchmark:dynamic
|
|
600
|
+
# Specify number of routes
|
|
601
|
+
bun run benchmark --routes=100
|
|
602
|
+
bun run benchmark:static --routes=100
|
|
603
|
+
bun run benchmark:dynamic --routes=100
|
|
604
|
+
|
|
605
|
+
# Memory usage benchmark
|
|
606
|
+
bun run benchmark:memory
|
|
607
|
+
bun run benchmark:memory --routes=200
|
|
608
|
+
|
|
609
|
+
# Stress test (concurrent matching)
|
|
610
|
+
bun run benchmark:stress
|
|
611
|
+
bun run benchmark:stress --routes=500
|
|
612
|
+
bun run benchmark:stress --routes=1000
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
<span id="testing"></span>
|
|
616
|
+
|
|
617
|
+
## ✅ Testing
|
|
618
|
+
|
|
619
|
+
Run the comprehensive test suite:
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
bun test
|
|
623
|
+
# or for coverage report
|
|
624
|
+
bun run coverage
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
The coverage report can be found in the `coverage/` directory ([`coverage/index.html`](c:\Users\lior3\Development\liodex\extreme-router\coverage\index.html)).
|
|
628
|
+
|
|
629
|
+
**100% code coverage** is ensured.
|
|
630
|
+
|
|
631
|
+
<span id="acknowledgments"></span>
|
|
632
|
+
|
|
633
|
+
## 🙏 Acknowledgments
|
|
634
|
+
|
|
635
|
+
Extreme Router draws inspiration from the high-level routing concepts and per-route register/store design of [Medley Router](https://github.com/medleyjs/router). Sincere thanks to the Medley Router authors for their foundational ideas.
|
|
636
|
+
|
|
637
|
+
<span id="contributing"></span>
|
|
638
|
+
|
|
639
|
+
## 🤝 Contributing
|
|
640
|
+
|
|
641
|
+
Contributions are welcome!
|
|
642
|
+
Please read our [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines on development, testing, benchmarking, and submitting pull requests.
|
|
643
|
+
|
|
644
|
+
<span id="license"></span>
|
|
645
|
+
|
|
646
|
+
## 📜 License
|
|
647
|
+
|
|
648
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"index.cjs": {
|
|
4
|
+
"minified": "13.44 KB",
|
|
5
|
+
"gzipped": "4.06 KB",
|
|
6
|
+
"minifiedBytes": 13763,
|
|
7
|
+
"gzippedBytes": 4161
|
|
8
|
+
},
|
|
9
|
+
"index.js": {
|
|
10
|
+
"minified": "12.87 KB",
|
|
11
|
+
"gzipped": "3.81 KB",
|
|
12
|
+
"minifiedBytes": 13180,
|
|
13
|
+
"gzippedBytes": 3901
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"total": {
|
|
17
|
+
"minified": 26943,
|
|
18
|
+
"gzipped": 8062,
|
|
19
|
+
"minifiedKB": "26.31 KB",
|
|
20
|
+
"gzippedKB": "7.87 KB"
|
|
21
|
+
}
|
|
22
|
+
}
|