router-http 1.0.12 → 1.0.13

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.
Files changed (3) hide show
  1. package/README.md +72 -22
  2. package/package.json +2 -1
  3. package/src/index.js +87 -54
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  A middleware style router, similar to [express@router](https://github.com/pillarjs/router), plus:
8
8
 
9
- - Faster (x3 compared with Express).
9
+ - Faster (~30% more requests per second than Express).
10
10
  - Maintained and well tested.
11
- - Smaller (1.4 kB).
11
+ - Smaller (1.8 kB).
12
12
 
13
13
  Don't get me wrong: The original Express router is a piece of art. I used it for years and I just considered create this library after experienced a bug that never was addressed in the stable version due to the [lack of maintenance](https://github.com/pillarjs/router/pull/60).
14
14
 
@@ -110,20 +110,68 @@ They are only will add if the condition is satisfied.
110
110
 
111
111
  Middlewares always run **before** route handlers, regardless of declaration order. This means you can declare `.use()` before or after `.get()` / `.post()` / etc., and the middleware will still execute first.
112
112
 
113
- ### Prefixing routes
113
+ ### Nested routers
114
114
 
115
- In case you need you can prefix all the routes:
115
+ You can use a router as a middleware for another router. This is useful for prefixing routes or for modularizing your application.
116
+
117
+ When a sub-router is used as a middleware, it will only handle requests that match its prefix. If no route matches inside the sub-router, it will automatically call `next()` to pass control back to the parent router.
116
118
 
117
119
  ```js
118
- routes.get('/', (req, res) => res.end('Welcome to my API!'))
120
+ const createRouter = require('router-http')
121
+ const http = require('http')
119
122
 
120
- /**
121
- * Prefix all routes with the API version
122
- */
123
+ const final = (err, req, res) => {
124
+ res.statusCode = err ? 500 : 404
125
+ res.end(err ? err.message : 'Not Found')
126
+ }
127
+
128
+ // 1. Create a sub-router for API v1
129
+ const v1 = createRouter(final)
130
+ v1.get('/info', (req, res) => res.end('v1 info'))
131
+
132
+ // 2. Create another sub-router for API v2
133
+ const v2 = createRouter(final)
134
+ v2.get('/info', (req, res) => res.end('v2 info'))
135
+
136
+ // 3. Create the main router and mount sub-routers
123
137
  const router = createRouter(final)
138
+
124
139
  router
125
- .use('/latest', routes)
126
- .use('/v1', routes)
140
+ .use('/v1', v1)
141
+ .use('/v2', v2)
142
+ .get('/', (req, res) => res.end('Welcome to the main router'))
143
+
144
+ http.createServer(router).listen(3000)
145
+ ```
146
+
147
+ #### Exit Current Router
148
+
149
+ You can use `next('router')` to skip all remaining handlers in the current router and pass control back to the parent router. This is useful for conditional routing, such as a "Beta" router that only handles requests for certain users:
150
+
151
+ ```js
152
+ const beta = createRouter(final)
153
+
154
+ // Middleware to check if the user is a beta tester
155
+ beta.use((req, res, next) => {
156
+ if (!req.isBetaTester) return next('router')
157
+ next()
158
+ })
159
+
160
+ beta.get('/search', (req, res) => {
161
+ res.end('Using the new AI-powered search engine!')
162
+ })
163
+
164
+ const router = createRouter(final)
165
+
166
+ // Mount the beta router
167
+ router.use('/v1', beta)
168
+
169
+ // This will be reached if:
170
+ // 1. The user is NOT a beta tester (next('router') was called)
171
+ // 2. Or the path didn't match anything inside the beta router
172
+ router.get('/v1/search', (req, res) => {
173
+ res.end('Using the classic search engine.')
174
+ })
127
175
  ```
128
176
 
129
177
  ### Using the router
@@ -136,30 +184,32 @@ const server = http.createServer(router)
136
184
 
137
185
  ## Benchmark
138
186
 
139
- **express@4.18.2**
187
+ With all the improvements, router-http is approximately 30% faster than the express router:
188
+
189
+ **express@5.2.1**
140
190
 
141
191
  ```
142
192
  Running 30s test @ http://localhost:3000/user/123
143
193
  8 threads and 100 connections
144
194
  Thread Stats Avg Stdev Max +/- Stdev
145
- Latency 4.12ms 653.26us 21.71ms 89.35%
146
- Req/Sec 2.93k 159.60 5.99k 84.75%
147
- 700421 requests in 30.06s, 102.87MB read
148
- Requests/sec: 23304.22
149
- Transfer/sec: 3.42MB
195
+ Latency 1.23ms 1.40ms 96.27ms 99.61%
196
+ Req/Sec 10.15k 615.89 11.07k 86.24%
197
+ 2430687 requests in 30.10s, 356.98MB read
198
+ Requests/sec: 80752.48
199
+ Transfer/sec: 11.86MB
150
200
  ```
151
201
 
152
- **router-http@1.0.0**
202
+ **router-http**
153
203
 
154
204
  ```
155
205
  Running 30s test @ http://localhost:3000/user/123
156
206
  8 threads and 100 connections
157
207
  Thread Stats Avg Stdev Max +/- Stdev
158
- Latency 1.33ms 690.36us 30.28ms 97.16%
159
- Req/Sec 9.27k 1.09k 11.76k 89.58%
160
- 2214097 requests in 30.02s, 276.61MB read
161
- Requests/sec: 73754.53
162
- Transfer/sec: 9.21MB
208
+ Latency 0.97ms 1.27ms 84.82ms 99.77%
209
+ Req/Sec 12.91k 1.07k 14.67k 71.51%
210
+ 3092927 requests in 30.10s, 386.40MB read
211
+ Requests/sec: 102751.65
212
+ Transfer/sec: 12.84MB
163
213
  ```
164
214
 
165
215
  See more details, check [benchmark](/benchmark) section.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "router-http",
3
3
  "description": "Simple HTTP router compatible with Express",
4
4
  "homepage": "https://github.com/Kikobeats/router-http",
5
- "version": "1.0.12",
5
+ "version": "1.0.13",
6
6
  "main": "src/index.js",
7
7
  "author": {
8
8
  "email": "josefrancisco.verdu@gmail.com",
@@ -29,6 +29,7 @@
29
29
  "routes"
30
30
  ],
31
31
  "dependencies": {
32
+ "null-prototype-object": "~1.2.5",
32
33
  "trouter": "~4.0.0"
33
34
  },
34
35
  "devDependencies": {
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const NullProtoObj = require('null-prototype-object')
3
4
  const { Trouter } = require('trouter')
4
5
 
5
6
  const requiredFinalHandler = () => {
@@ -18,18 +19,18 @@ const value = x => {
18
19
 
19
20
  const parse = ({ url }) => {
20
21
  const index = url.indexOf('?', 1)
21
- const obj = { pathname: url, query: null, search: null }
22
- if (index !== -1) {
23
- obj.search = url.substring(index)
24
- obj.query = obj.search.substring(1)
25
- obj.pathname = url.substring(0, index)
22
+ if (index === -1) return { pathname: url, query: null, search: null }
23
+ const search = url.substring(index)
24
+ return {
25
+ pathname: url.substring(0, index),
26
+ query: search.substring(1),
27
+ search
26
28
  }
27
- return obj
28
29
  }
29
30
 
30
31
  const mutate = (str, req) => {
31
- req.url = req.url.substring(str.length) ?? '/'
32
- req.path = req.path.substring(str.length) ?? '/'
32
+ req.url = req.url.substring(str.length) || '/'
33
+ req.path = req.path.substring(str.length) || '/'
33
34
  }
34
35
 
35
36
  class Router extends Trouter {
@@ -46,7 +47,7 @@ class Router extends Trouter {
46
47
  /**
47
48
  * Middleware for specific routes
48
49
  */
49
- #middlewaresBy = []
50
+ #middlewaresBy = new NullProtoObj()
50
51
 
51
52
  /**
52
53
  * Middleware declaration, where the page is optional
@@ -64,65 +65,97 @@ class Router extends Trouter {
64
65
  page = lead(page)
65
66
  fns.filter(Boolean).forEach(fn => {
66
67
  const array = this.#middlewaresBy[page] ?? []
67
- // eslint-disable-next-line no-sequences
68
- array.length > 0 || array.push((r, _, nxt) => (mutate(page, r), nxt()))
68
+ if (array.length === 0) {
69
+ array.push((r, _, nxt) => {
70
+ mutate(page, r)
71
+ nxt()
72
+ })
73
+ }
69
74
  this.#middlewaresBy[page] = array.concat(fn)
70
75
  })
71
76
  }
72
77
  return this
73
78
  }
74
79
 
75
- handler = (req, res, info) => {
76
- info = info ?? parse(req)
77
- let fns = []
78
- let middlewares = this.#middlewares
79
- const route = this.find(req.method, info.pathname)
80
- const page = value((req.path = info.pathname))
81
- if (this.#middlewaresBy[page] !== undefined) {
82
- middlewares = middlewares.concat(this.#middlewaresBy[page])
83
- }
84
- if (route) {
85
- fns = route.handlers
86
- req.params = { ...req.params, ...route.params }
80
+ handler = (req, res, next) => {
81
+ const info = parse(req)
82
+ const pathname = info.pathname
83
+ req.path = pathname
84
+ const route = this.find(req.method, pathname)
85
+ const page = value(pathname)
86
+
87
+ const m = this.#middlewares
88
+ const mb = this.#middlewaresBy[page]
89
+ const f = route.handlers.length > 0 ? route.handlers : null
90
+
91
+ if (f) {
92
+ req.params = req.params
93
+ ? { ...req.params, ...route.params }
94
+ : route.params
95
+ } else {
96
+ req.params = req.params || {}
87
97
  }
88
- fns.push(this.unhandler)
89
- req.search = req.query ?? info.search
90
- req.query = req.query ?? info.query
91
- // Exit if only a single function
98
+
99
+ req.search = req.query || info.search
100
+ req.query = req.query || info.query
101
+
92
102
  let index = 0
93
- let size = middlewares.length
94
- const num = fns.length
95
- if (size === index && num === 1) return fns[0](undefined, req, res)
96
-
97
- // Otherwise loop thru all middlware
98
- const next = err => (err ? this.unhandler(err, req, res, next) : loop())
99
-
100
- const loop = () =>
101
- res.writableEnded ||
102
- (index < size &&
103
- (async () => {
104
- try {
105
- const mware = middlewares[index++]
106
- const result =
107
- index === size
108
- ? mware(undefined, req, res, next)
109
- : mware(req, res, next)
110
- if (result && typeof result.then === 'function') {
111
- await result
112
- }
113
- } catch (err) {
114
- return this.unhandler(err, req, res, next)
103
+ let sync = 0
104
+
105
+ const mLen = m.length
106
+ const mbLen = mb ? mb.length : 0
107
+ const fLen = f ? f.length : 0
108
+ const total = mLen + mbLen + fLen
109
+
110
+ const _next = err => {
111
+ if (err === 'router') {
112
+ if (next) return next()
113
+ index = total
114
+ err = undefined
115
+ }
116
+ if (err) return this.unhandler(err, req, res, next)
117
+ if (++sync > 100) {
118
+ sync = 0
119
+ return setImmediate(loop)
120
+ }
121
+ loop()
122
+ }
123
+
124
+ const loop = () => {
125
+ if (index < total) {
126
+ if (res.writableEnded) return
127
+ const i = index++
128
+ const mware =
129
+ i < mLen
130
+ ? m[i]
131
+ : i < mLen + mbLen
132
+ ? mb[i - mLen]
133
+ : f[i - mLen - mbLen]
134
+
135
+ try {
136
+ const result = mware(req, res, _next)
137
+ if (result && typeof result.then === 'function') {
138
+ return result.then(undefined, _next)
115
139
  }
116
- })())
140
+ } catch (err) {
141
+ return _next(err)
142
+ }
143
+ return
144
+ }
145
+
146
+ if (res.writableEnded) return
147
+
148
+ if (next) return next()
149
+
150
+ this.unhandler(undefined, req, res, _next)
151
+ }
117
152
 
118
- middlewares = middlewares.concat(fns)
119
- size += num
120
- loop() // init
153
+ loop()
121
154
  }
122
155
  }
123
156
 
124
157
  module.exports = (finalhandler = requiredFinalHandler()) => {
125
158
  const router = new Router(finalhandler)
126
- const handler = (req, res) => router.handler(req, res)
159
+ const handler = (req, res, next) => router.handler(req, res, next)
127
160
  return Object.assign(handler, router)
128
161
  }