ultimate-express 1.2.0 → 1.2.2

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
@@ -16,7 +16,7 @@ To make sure µExpress matches behavior of Express in all cases, we run all test
16
16
 
17
17
  Similar projects based on uWebSockets:
18
18
 
19
- - `express` on Bun - since Bun uses uWS for its HTTP module, Express is about 2.5 times faster than on Node.js with 27k req/sec instead of 10k req/sec normally, but still slower than µExpress at 70k req/sec because it doesn't do uWS-specific optimizations.
19
+ - `express` on Bun - since Bun uses uWS for its HTTP module, Express is about 2-3 times faster than on Node.js, but still slower than µExpress because it doesn't do uWS-specific optimizations.
20
20
  - `hyper-express` - while having a similar API to Express, it's very far from being a drop-in replacement, and implements most of the functionality differently. This creates a lot of random quirks and issues, making the switch quite difficult. Built in middlewares are also very different.
21
21
  - `uwebsockets-express` - this library is closer to being a drop-in replacement, but misses a lot of APIs, depends on Express by calling it's methods under the hood and doesn't try to optimize routing by using native uWS router.
22
22
 
@@ -24,6 +24,8 @@ Similar projects based on uWebSockets:
24
24
 
25
25
  Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Etag was disabled in both Express and µExpress. Tested on Ubuntu 22.04, Node.js 20.17.0, AMD Ryzen 5 3600, 64GB RAM.
26
26
 
27
+ ### Test results
28
+
27
29
  | Test | Express req/sec | µExpress req/sec | Express throughput | µExpress throughput | µExpress speedup |
28
30
  | --------------------------------------------- | --------------- | ---------------- | ------------------ | ------------------- | ---------------- |
29
31
  | routing/simple-routes (/) | 10.90k | 70.10k | 2.04 MB/sec | 11.57 MB/sec | **6.43X** |
@@ -34,6 +36,30 @@ Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Etag was di
34
36
  | engines/ejs (/test) | 5.92k | 41.64k | 2.40 MB/sec | 16.55 MB/sec | **7.03X** |
35
37
  | middlewares/body-urlencoded (/abc) | 7.90k | 29.90k | 1.64 MB/sec | 5.36 MB/sec | **3.78X** |
36
38
 
39
+ ### Performance against other frameworks
40
+
41
+ Tested using [bun-http-framework-benchmark](https://github.com/dimdenGD/bun-http-framework-benchmark)
42
+
43
+ | Framework | Runtime | Average | Ping | Query | Body |
44
+ | ---------------- | ------- | ------- | ---------- | ---------- | ---------- |
45
+ | uws | node | 94,296.49 | 108,551.92 | 104,756.22 | 69,581.33 |
46
+ | bun | bun | 74,824.52 | 85,839.42 | 74,668.88 | 63,965.26 |
47
+ | elysia | bun | 72,112.447 | 82,589.71 | 69,356.08 | 64,391.55 |
48
+ | hyper-express | node | 66,356.707 | 80,002.53 | 69,953.76 | 49,113.83 |
49
+ | hono | bun | 63,944.627 | 74,550.47 | 62,810.28 | 54,473.13 |
50
+ | **ultimate-express** | **node** | **44,081.737** | **51,753.24** | **48,389.84** | **32,102.13** |
51
+ | oak | deno | 40,878.467 | 68,429.24 | 28,541.99 | 25,664.17 |
52
+ | express | bun | 35,937.977 | 41,329.97 | 34,339.79 | 32,144.17 |
53
+ | h3 | node | 35,423.263 | 41,243.68 | 34,429.26 | 30,596.85 |
54
+ | fastify | node | 33,094.62 | 40,147.67 | 40,076.35 | 19,059.84 |
55
+ | oak | bun | 32,705.36 | 35,856.59 | 32,116.4 | 30,143.09 |
56
+ | hono | node | 26,576.02 | 36,215.35 | 34,656.12 | 8,856.59 |
57
+ | acorn | deno | 24,476.67 | 29,690.42 | 22,254.82 | 21,484.77 |
58
+ | koa | node | 24,045.08 | 28,202.12 | 24,590.84 | 19,342.28 |
59
+ | express | node | 10,411.313 | 11,245.57 | 10,598.74 | 9,389.63 |
60
+
61
+ ### Performance on real-world application
62
+
37
63
  Also tested on a [real-world application](https://nekoweb.org) with templates, static files and dynamic pages with data from database, and showed 1.5-4X speedup in requests per second depending on the page.
38
64
 
39
65
  ## Differences from Express
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-express",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "The Ultimate Express. Fastest http server with full Express compatibility, based on uWebSockets.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -178,23 +178,12 @@ class Application extends Router {
178
178
 
179
179
  #createRequestHandler() {
180
180
  this.uwsApp.any('/*', async (res, req) => {
181
- const request = new this._request(req, res, this);
182
- const response = new this._response(res, request, this);
183
- request.res = response;
184
- response.req = request;
185
-
186
- res.onAborted(() => {
187
- const err = new Error('Request aborted');
188
- err.code = 'ECONNABORTED';
189
- response.aborted = true;
190
- response.socket.emit('error', err);
191
- });
192
-
193
- let matchedRoute = await this._routeRequest(request, response);
181
+ const { request, response } = this.handleRequest(res, req);
194
182
 
195
- if(!matchedRoute && !res.aborted && !response.headersSent) {
183
+ const matchedRoute = await this._routeRequest(request, response);
184
+ if(!matchedRoute && !response.headersSent && !response.aborted) {
196
185
  response.status(404);
197
- response.send(this._generateErrorPage(`Cannot ${request.method} ${request.path}`));
186
+ response.send(this._generateErrorPage(`Cannot ${request.method} ${request.path}`, false));
198
187
  }
199
188
  });
200
189
  }
package/src/router.js CHANGED
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom } = require("./utils.js");
17
+ const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized } = require("./utils.js");
18
18
  const Response = require("./response.js");
19
19
  const Request = require("./request.js");
20
20
  const { EventEmitter } = require("tseep");
@@ -138,7 +138,7 @@ module.exports = class Router extends EventEmitter {
138
138
  };
139
139
  routes.push(route);
140
140
  // normal routes optimization
141
- if(typeof route.pattern === 'string' && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
141
+ if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
142
142
  if(supportedUwsMethods.includes(method)) {
143
143
  const optimizedPath = this.#optimizeRoute(route, this._routes);
144
144
  if(optimizedPath) {
@@ -185,7 +185,8 @@ module.exports = class Router extends EventEmitter {
185
185
  this.#registerUwsRoute({
186
186
  ...cbroute,
187
187
  path: route.path + cbroute.path,
188
- pattern: route.path + cbroute.path
188
+ pattern: route.path + cbroute.path,
189
+ optimizedRouter: true
189
190
  }, optimizedPath);
190
191
  }
191
192
  }
@@ -239,6 +240,21 @@ module.exports = class Router extends EventEmitter {
239
240
  return optimizedPath;
240
241
  }
241
242
 
243
+ handleRequest(res, req) {
244
+ const request = new this._request(req, res, this);
245
+ const response = new this._response(res, request, this);
246
+ request.res = response;
247
+ response.req = request;
248
+ res.onAborted(() => {
249
+ const err = new Error('Connection closed');
250
+ err.code = 'ECONNRESET';
251
+ response.aborted = true;
252
+ response.socket.emit('error', err);
253
+ });
254
+
255
+ return { request, response };
256
+ }
257
+
242
258
  #registerUwsRoute(route, optimizedPath) {
243
259
  let method = route.method.toLowerCase();
244
260
  if(method === 'all') {
@@ -246,35 +262,34 @@ module.exports = class Router extends EventEmitter {
246
262
  } else if(method === 'delete') {
247
263
  method = 'del';
248
264
  }
265
+ if(!route.optimizedRouter && route.path.includes(":")) {
266
+ route.optimizedParams = route.path.match(/:(\w+)/g).map(p => p.slice(1));
267
+ }
249
268
  const fn = async (res, req) => {
250
- const request = new this._request(req, res, this);
251
- const response = new this._response(res, request, this);
252
- request.res = response;
253
- response.req = request;
254
- res.onAborted(() => {
255
- const err = new Error('Connection closed');
256
- err.code = 'ECONNRESET';
257
- response.aborted = true;
258
- response.socket.emit('error', err);
259
- });
260
-
261
- const routed = await this._routeRequest(request, response, 0, optimizedPath, true, route);
262
- if(routed) {
263
- return;
269
+ const { request, response } = this.handleRequest(res, req);
270
+ if(route.optimizedParams) {
271
+ request.optimizedParams = {};
272
+ for(let i = 0; i < route.optimizedParams.length; i++) {
273
+ request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
274
+ }
275
+ }
276
+ const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, route);
277
+ if(!matchedRoute && !response.headersSent && !response.aborted) {
278
+ response.status(404);
279
+ response.send(this._generateErrorPage(`Cannot ${request.method} ${request.path}`, false));
264
280
  }
265
- response.status(404);
266
- response.send(this._generateErrorPage(`Cannot ${request.method} ${request.path}`, false));
267
281
  };
268
282
  route.optimizedPath = optimizedPath;
269
- this.uwsApp[method](route.path, fn);
283
+ let replacedPath = route.path.replace(/:(\w+)/g, ':x');
284
+ this.uwsApp[method](replacedPath, fn);
270
285
  if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
271
- this.uwsApp[method](route.path + '/', fn);
286
+ this.uwsApp[method](replacedPath + '/', fn);
272
287
  if(method === 'get') {
273
- this.uwsApp.head(route.path + '/', fn);
288
+ this.uwsApp.head(replacedPath + '/', fn);
274
289
  }
275
290
  }
276
291
  if(method === 'get') {
277
- this.uwsApp.head(route.path, fn);
292
+ this.uwsApp.head(replacedPath, fn);
278
293
  }
279
294
  }
280
295
 
@@ -313,7 +328,9 @@ module.exports = class Router extends EventEmitter {
313
328
  #preprocessRequest(req, res, route) {
314
329
  return new Promise(async resolve => {
315
330
  req.route = route;
316
- if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*')) && route.pattern instanceof RegExp) {
331
+ if(route.optimizedParams) {
332
+ req.params = req.optimizedParams;
333
+ } else if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*')) && route.pattern instanceof RegExp) {
317
334
  let path = req.path;
318
335
  if(req._stack.length > 0) {
319
336
  path = path.replace(this.getFullMountpath(req), '');
@@ -324,31 +341,6 @@ module.exports = class Router extends EventEmitter {
324
341
  req.params = {...params, ...req.params};
325
342
  }
326
343
  }
327
-
328
- for(let param in req.params) {
329
- if(this.#paramCallbacks.has(param) && !req._gotParams.has(param)) {
330
- req._gotParams.add(param);
331
- const pcs = this.#paramCallbacks.get(param);
332
- for(let i = 0; i < pcs.length; i++) {
333
- const fn = pcs[i];
334
- await new Promise(resolveRoute => {
335
- const next = (thingamabob) => {
336
- if(thingamabob) {
337
- if(thingamabob === 'route') {
338
- return resolve('route');
339
- } else {
340
- this.#handleError(thingamabob, req, res);
341
- return resolve(false);
342
- }
343
- }
344
- return resolveRoute();
345
- };
346
- req.next = next;
347
- fn(req, res, next, req.params[param], param);
348
- });
349
- }
350
- }
351
- }
352
344
  } else {
353
345
  req.params = {};
354
346
  if(req._paramStack.length > 0) {
@@ -358,6 +350,31 @@ module.exports = class Router extends EventEmitter {
358
350
  }
359
351
  }
360
352
 
353
+ for(let param in req.params) {
354
+ if(this.#paramCallbacks.has(param) && !req._gotParams.has(param)) {
355
+ req._gotParams.add(param);
356
+ const pcs = this.#paramCallbacks.get(param);
357
+ for(let i = 0; i < pcs.length; i++) {
358
+ const fn = pcs[i];
359
+ await new Promise(resolveRoute => {
360
+ const next = (thingamabob) => {
361
+ if(thingamabob) {
362
+ if(thingamabob === 'route') {
363
+ return resolve('route');
364
+ } else {
365
+ this.#handleError(thingamabob, req, res);
366
+ return resolve(false);
367
+ }
368
+ }
369
+ return resolveRoute();
370
+ };
371
+ req.next = next;
372
+ fn(req, res, next, req.params[param], param);
373
+ });
374
+ }
375
+ }
376
+ }
377
+
361
378
  resolve(true);
362
379
  });
363
380
  }
package/src/utils.js CHANGED
@@ -74,6 +74,29 @@ function needsConversionToRegex(pattern) {
74
74
  pattern.includes(']');
75
75
  }
76
76
 
77
+ function canBeOptimized(pattern) {
78
+ if(pattern === '/*') {
79
+ return false;
80
+ }
81
+ if(pattern instanceof RegExp) {
82
+ return false;
83
+ }
84
+ if(
85
+ pattern.includes('*') ||
86
+ pattern.includes('?') ||
87
+ pattern.includes('+') ||
88
+ pattern.includes('(') ||
89
+ pattern.includes(')') ||
90
+ pattern.includes('{') ||
91
+ pattern.includes('}') ||
92
+ pattern.includes('[') ||
93
+ pattern.includes(']')
94
+ ) {
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+
77
100
  function acceptParams(str) {
78
101
  const parts = str.split(/ *; */);
79
102
  const ret = { value: parts[0], quality: 1, params: {} }
@@ -311,5 +334,6 @@ module.exports = {
311
334
  createETagGenerator,
312
335
  isRangeFresh,
313
336
  findIndexStartingFrom,
314
- fastQueryParse
337
+ fastQueryParse,
338
+ canBeOptimized
315
339
  };