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/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
+ }