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