ultimate-express 1.4.8 → 1.4.10

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
@@ -35,6 +35,7 @@ Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Tested on U
35
35
  | middlewares/express-static (/static/index.js) | 6.58k | 32.45k | 10.15 MB/sec | 49.43 MB/sec | **4.87X** |
36
36
  | engines/ejs (/test) | 5.50k | 40.82k | 2.45 MB/sec | 18.38 MB/sec | **7.42X** |
37
37
  | middlewares/body-urlencoded (/abc) | 8.07k | 50.52k | 1.68 MB/sec | 10.78 MB/sec | **6.26X** |
38
+ | middlewares/compression-file (/small-file) | 4.81k | 14.92k | 386 MB/sec | 1.17 GB/sec | **3.10X** |
38
39
 
39
40
  ### Performance against other frameworks
40
41
 
@@ -101,7 +102,7 @@ app.listen(3000, () => {
101
102
  ```
102
103
 
103
104
  - This also applies to non-SSL HTTP too. Do not create http server manually, use `app.listen()` instead.
104
- - NodeJS max header size is 16384 byte, uWebSockets 4096 byte, if you need long headers set the env variable `UWS_HTTP_MAX_HEADERS_SIZE`
105
+ - Node.JS max header size is 16384 bytes, while uWebSockets by default is 4096 bytes, so if you need longer headers set the env variable `UWS_HTTP_MAX_HEADERS_SIZE` to max byte count you need.
105
106
 
106
107
  ## Performance tips
107
108
 
@@ -327,6 +328,8 @@ Almost all middlewares that are compatible with Express are compatible with µEx
327
328
  - ✅ [passport](https://www.npmjs.com/package/passport)
328
329
  - ✅ [morgan](https://www.npmjs.com/package/morgan)
329
330
  - ✅ [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express)
331
+ - ✅ [graphql-http](https://www.npmjs.com/package/graphql-http)
332
+ - ✅ [better-sse](https://www.npmjs.com/package/better-sse)
330
333
 
331
334
  Middlewares and modules that are confirmed to not work:
332
335
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-express",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
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": {
@@ -67,6 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@codechecks/client": "^0.1.12",
70
+ "better-sse": "^0.14.1",
70
71
  "body-parser": "^1.20.3",
71
72
  "compression": "^1.8.0",
72
73
  "cookie-parser": "^1.4.6",
@@ -74,6 +75,7 @@
74
75
  "cors": "^2.8.5",
75
76
  "ejs": "^3.1.10",
76
77
  "errorhandler": "^1.5.1",
78
+ "eventsource": "^3.0.6",
77
79
  "exit-hook": "^2.2.1",
78
80
  "express": "latest-4",
79
81
  "express-art-template": "^1.0.1",
@@ -86,6 +88,7 @@
86
88
  "express-session": "^1.18.0",
87
89
  "express-subdomain": "^1.0.6",
88
90
  "formdata-node": "^6.0.3",
91
+ "graphql-http": "^1.22.4",
89
92
  "helmet": "^8.1.0",
90
93
  "method-override": "^3.0.0",
91
94
  "morgan": "^1.10.0",
@@ -31,7 +31,7 @@ module.exports = function compileDeclarative(cb, app) {
31
31
 
32
32
  const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
33
33
 
34
- if(tokens.some(token => ['throw', 'new', 'await'].includes(token.value))) {
34
+ if(tokens.some(token => ['throw', 'new', 'await', 'return'].includes(token.value))) {
35
35
  return false;
36
36
  }
37
37
 
@@ -186,7 +186,7 @@ module.exports = function compileDeclarative(cb, app) {
186
186
  if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
187
187
  return false;
188
188
  }
189
- const sameHeader = headers.find(header => header[0] === call.arguments[0].value);
189
+ const sameHeader = headers.find(header => header[0].toLowerCase() === call.arguments[0].value.toLowerCase());
190
190
  if(sameHeader) {
191
191
  sameHeader[1] = call.arguments[1].value;
192
192
  } else {
@@ -201,8 +201,18 @@ module.exports = function compileDeclarative(cb, app) {
201
201
  }
202
202
 
203
203
  // get body
204
+ let sendUsed = false;
204
205
  for(let call of callExprs) {
205
206
  if(call.obj.propertyName === 'send' || call.obj.propertyName === 'end') {
207
+ if(sendUsed) {
208
+ return false;
209
+ }
210
+ if(call.obj.propertyName === 'send') {
211
+ const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
212
+ if(index === -1) {
213
+ headers.push(['content-type', 'text/html; charset=utf-8']);
214
+ }
215
+ }
206
216
  const arg = call.arguments[0];
207
217
  if(arg) {
208
218
  if(arg.type === 'Literal') {
@@ -212,6 +222,17 @@ module.exports = function compileDeclarative(cb, app) {
212
222
  let val = arg.value;
213
223
  if(val === null) {
214
224
  val = '';
225
+ const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
226
+ if(index !== -1) {
227
+ headers.splice(index, 1);
228
+ }
229
+ }
230
+ if(typeof val === 'boolean') {
231
+ if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
232
+ headers.push(['content-type', 'application/json; charset=utf-8']);
233
+ } else {
234
+ headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
235
+ }
215
236
  }
216
237
  body.push({type: 'text', value: val});
217
238
  } else if(arg.type === 'TemplateLiteral') {
@@ -300,7 +321,11 @@ module.exports = function compileDeclarative(cb, app) {
300
321
  return false;
301
322
  }
302
323
 
303
- headers.push(['content-type', 'application/json; charset=utf-8']);
324
+ if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
325
+ headers.push(['content-type', 'application/json; charset=utf-8']);
326
+ } else {
327
+ headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
328
+ }
304
329
  body.push({
305
330
  type: 'text',
306
331
  value:
@@ -315,6 +340,7 @@ module.exports = function compileDeclarative(cb, app) {
315
340
  return false;
316
341
  }
317
342
  }
343
+ sendUsed = true;
318
344
  }
319
345
  }
320
346
 
@@ -329,6 +355,9 @@ module.exports = function compileDeclarative(cb, app) {
329
355
  if(header[0].toLowerCase() === 'content-length') {
330
356
  return false;
331
357
  }
358
+ if(header[0].toLowerCase() === 'content-type' && header[1].includes('text/') && !header[1].includes(';')) {
359
+ header[1] += '; charset=utf-8';
360
+ }
332
361
  decRes = decRes.writeHeader(header[0], header[1]);
333
362
  }
334
363
 
@@ -169,7 +169,7 @@ function createBodyParser(defaultType, beforeReturn) {
169
169
 
170
170
  req.body = new NullObject();
171
171
 
172
- // skip reading body for non-json content type
172
+ // skip reading body for no content type
173
173
  if(!type) {
174
174
  return next();
175
175
  }
@@ -281,6 +281,10 @@ const json = createBodyParser('application/json', function(req, res, next, optio
281
281
  return next(new Error('Invalid body'));
282
282
  }
283
283
  }
284
+ if(buf.length === 0) {
285
+ req.body = {};
286
+ return next();
287
+ }
284
288
  try {
285
289
  req.body = JSON.parse(buf.toString(), options.reviver);
286
290
  } catch(e) {
package/src/response.js CHANGED
@@ -71,6 +71,7 @@ module.exports = class Response extends Writable {
71
71
  #socket = null;
72
72
  #pendingChunks = [];
73
73
  #lastWriteChunkTime = 0;
74
+ #writeTimeout = null;
74
75
  constructor(res, req, app) {
75
76
  super();
76
77
  this._req = req;
@@ -155,11 +156,28 @@ module.exports = class Response extends Writable {
155
156
  const now = Date.now();
156
157
  // the first chunk is sent immediately (!this.#lastWriteChunkTime)
157
158
  // the other chunks are sent when watermark is reached (size >= HIGH_WATERMARK)
158
- // or if elapsed 100ms of last send (now - this.#lastWriteChunkTime > 100)
159
- if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime > 100) {
159
+ // or if elapsed 50ms of last send (now - this.#lastWriteChunkTime > 50)
160
+ if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime > 50) {
160
161
  this._res.write(Buffer.concat(this.#pendingChunks, size));
161
162
  this.#pendingChunks = [];
162
163
  this.#lastWriteChunkTime = now;
164
+ if(this.#writeTimeout) {
165
+ clearTimeout(this.#writeTimeout);
166
+ this.#writeTimeout = null;
167
+ }
168
+ } else if(!this.#writeTimeout) {
169
+ this.#writeTimeout = setTimeout(() => {
170
+ this.#writeTimeout = null;
171
+ if(!this.finished && !this.aborted) this._res.cork(() => {
172
+ if(this.#pendingChunks.length) {
173
+ const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
174
+ this._res.write(Buffer.concat(this.#pendingChunks, size));
175
+ this.#pendingChunks = [];
176
+ this.#lastWriteChunkTime = now;
177
+ }
178
+ });
179
+ }, 50);
180
+ this.#writeTimeout.unref();
163
181
  }
164
182
  this.writingChunk = false;
165
183
  callback(null);
@@ -201,12 +219,13 @@ module.exports = class Response extends Writable {
201
219
  this.statusText = statusMessage;
202
220
  }
203
221
  if(!headers) {
204
- if(!statusMessage) return;
222
+ if(!statusMessage) return this;
205
223
  headers = statusMessage;
206
224
  }
207
225
  for(let header in headers) {
208
226
  this.set(header, headers[header]);
209
227
  }
228
+ return this;
210
229
  }
211
230
  writeHeaders(utf8) {
212
231
  for(const header in this.headers) {
@@ -225,9 +244,6 @@ module.exports = class Response extends Writable {
225
244
  this._res.writeHeader(header, value);
226
245
  }
227
246
  }
228
- if(!this.headers['content-type']) {
229
- this._res.writeHeader('content-type', 'text/html' + (utf8 ? `; charset=utf-8` : ''));
230
- }
231
247
  this.headersSent = true;
232
248
  }
233
249
  _implicitHeader() {
@@ -267,6 +283,8 @@ module.exports = class Response extends Writable {
267
283
  this._res.end();
268
284
  this.finished = true;
269
285
  if(this.socketExists) this.socket.emit('close');
286
+ this.emit('finish');
287
+ this.emit('close');
270
288
  return;
271
289
  }
272
290
  }
@@ -305,6 +323,7 @@ module.exports = class Response extends Writable {
305
323
  const isBuffer = Buffer.isBuffer(body);
306
324
  if(body === null || body === undefined) {
307
325
  body = '';
326
+ return this.end(body);
308
327
  } else if(typeof body === 'object' && !isBuffer) {
309
328
  return this.json(body);
310
329
  } else if(typeof body === 'number') {
@@ -313,18 +332,25 @@ module.exports = class Response extends Writable {
313
332
  return this.status(body).send(arguments[1]);
314
333
  } else {
315
334
  deprecated('res.send(status)', 'res.sendStatus(status)');
335
+ if(!this.headers['content-type']) {
336
+ this.headers['content-type'] = 'text/plain; charset=utf-8';
337
+ }
316
338
  return this.sendStatus(body);
317
339
  }
340
+ } else if(typeof body === 'boolean') {
341
+ return this.json(body);
318
342
  } else if(!isBuffer) {
319
343
  body = String(body);
320
344
  }
321
- if(typeof body === 'string') {
345
+ if(typeof body === 'string' && !isBuffer) {
322
346
  const contentType = this.headers['content-type'];
323
347
  if(!contentType) {
324
348
  this.headers['content-type'] = 'text/html; charset=utf-8';
325
349
  } else if(!contentType.includes(';')) {
326
350
  this.headers['content-type'] += '; charset=utf-8';
327
351
  }
352
+ } else {
353
+ this.headers['content-type'] = 'application/octet-stream';
328
354
  }
329
355
  return this.end(body);
330
356
  }
@@ -593,6 +619,9 @@ module.exports = class Response extends Writable {
593
619
  getHeader(field) {
594
620
  return this.get(field);
595
621
  }
622
+ getHeaders(){
623
+ return this.headers;
624
+ }
596
625
  removeHeader(field) {
597
626
  delete this.headers[field.toLowerCase()];
598
627
  return this;
@@ -696,11 +725,7 @@ module.exports = class Response extends Writable {
696
725
  jsonp(object) {
697
726
  let callback = this.req.query[this.app.get('jsonp callback name')];
698
727
  let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
699
-
700
- if(!this.headers['content-type']) {
701
- this.headers['content-type'] = 'application/javascript; charset=utf-8';
702
- this.headers['X-Content-Type-Options'] = 'nosniff';
703
- }
728
+ let js = false;
704
729
 
705
730
  if(Array.isArray(callback)) {
706
731
  callback = callback[0];
@@ -718,6 +743,13 @@ module.exports = class Response extends Writable {
718
743
  .replace(/\u2029/g, '\\u2029')
719
744
  }
720
745
  body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
746
+ js = true;
747
+ }
748
+
749
+
750
+ if(!this.headers['content-type']) {
751
+ this.headers['content-type'] = `${js ? 'text/javascript' : 'application/json'}; charset=utf-8`;
752
+ if(js) this.headers['X-Content-Type-Options'] = 'nosniff';
721
753
  }
722
754
 
723
755
  return this.send(body);
@@ -742,7 +774,25 @@ module.exports = class Response extends Writable {
742
774
  this.location(url);
743
775
  this.status(status);
744
776
  this.headers['content-type'] = 'text/plain; charset=utf-8';
745
- return this.send(`${statuses.message[status] ?? status}. Redirecting to ${url}`);
777
+ let body;
778
+ // Support text/{plain,html} by default
779
+ this.format({
780
+ text: function() {
781
+ body = statuses.message[status] + '. Redirecting to ' + url
782
+ },
783
+ html: function() {
784
+ let u = escapeHtml(url);
785
+ body = '<p>' + statuses.message[status] + '. Redirecting to ' + u + '</p>'
786
+ },
787
+ default: function() {
788
+ body = '';
789
+ }
790
+ });
791
+ if (this.req.method === 'HEAD') {
792
+ this.end();
793
+ } else {
794
+ this.end(body);
795
+ }
746
796
  }
747
797
 
748
798
  type(type) {
@@ -755,6 +805,11 @@ module.exports = class Response extends Writable {
755
805
  contentType = this.type;
756
806
 
757
807
  vary(field) {
808
+ // checks for back-compat
809
+ if (!field || (Array.isArray(field) && !field.length)) {
810
+ deprecate('res.vary(): Provide a field name');
811
+ return this;
812
+ }
758
813
  vary(this, field);
759
814
  return this;
760
815
  }
@@ -766,4 +821,4 @@ module.exports = class Response extends Writable {
766
821
  get writableFinished() {
767
822
  return this.finished;
768
823
  }
769
- }
824
+ }
package/src/router.js CHANGED
@@ -180,7 +180,7 @@ module.exports = class Router extends EventEmitter {
180
180
  optimizedPathToRouter = optimizedPathToRouter.slice(0, -1); // remove last element, which is the router itself
181
181
  if(optimizedPathToRouter) {
182
182
  // wait for routes in router to be registered
183
- setTimeout(() => {
183
+ const t = setTimeout(() => {
184
184
  if(!this.listenCalled) {
185
185
  return; // can only optimize router whos parent is listening
186
186
  }
@@ -208,6 +208,7 @@ module.exports = class Router extends EventEmitter {
208
208
  }
209
209
  }
210
210
  }, 100);
211
+ t.unref();
211
212
  }
212
213
  // only 1 router can be optimized per route
213
214
  break;
@@ -354,7 +355,10 @@ module.exports = class Router extends EventEmitter {
354
355
  path = path.slice(0, -1);
355
356
  }
356
357
  let match = pattern.exec(path);
357
- const obj = match?.groups ?? new NullObject();
358
+ if( match?.groups ){
359
+ return match.groups;
360
+ }
361
+ const obj = new NullObject();
358
362
  for(let i = 1; i < match.length; i++) {
359
363
  obj[i - 1] = match[i];
360
364
  }
@@ -526,6 +530,9 @@ module.exports = class Router extends EventEmitter {
526
530
  req._opPath += '/';
527
531
  }
528
532
  const routed = await callback._routeRequest(req, res, 0);
533
+ if (req._error) {
534
+ req._errorKey = route.routeKey;
535
+ }
529
536
  if(routed) return resolve(true);
530
537
  next();
531
538
  } else {
package/src/view.js CHANGED
@@ -59,26 +59,25 @@ module.exports = class View {
59
59
  this.path += this.ext;
60
60
  }
61
61
  } else {
62
- this.path = path.join(this.root, fileName);
62
+ this.path = this.lookup(fileName);
63
63
  }
64
64
  }
65
65
 
66
66
  lookup(name) {
67
- let path;
67
+ let _path;
68
68
  let roots = [].concat(this.root);
69
- for (let i = 0; i < roots.length && !path; i++) {
69
+ for (let i = 0; i < roots.length && !_path; i++) {
70
70
  const root = roots[i];
71
71
 
72
72
  // resolve the path
73
73
  const loc = path.resolve(root, name);
74
74
  const dir = path.dirname(loc);
75
75
  const file = path.basename(loc);
76
-
76
+
77
77
  // resolve the file
78
- path = this.resolve(dir, file);
78
+ _path = this.resolve(dir, file);
79
79
  }
80
-
81
- return path;
80
+ return _path;
82
81
  }
83
82
 
84
83
  // ill be real idk what exactly this does but express implements it this way
@@ -101,19 +100,19 @@ module.exports = class View {
101
100
  const ext = this.ext;
102
101
 
103
102
  // <path>.<ext>
104
- let path = path.join(dir, file);
105
- let stat = tryStat(path);
103
+ let _path = path.join(dir, file);
104
+ let stat = tryStat(_path);
106
105
 
107
106
  if(stat && stat.isFile()) {
108
- return path;
107
+ return _path;
109
108
  }
110
109
 
111
110
  // <path>/index.<ext>
112
- path = path.join(dir, path.basename(file, ext) + ext);
113
- stat = tryStat(path);
111
+ _path = path.join(dir, path.basename(file, ext), 'index' + ext);
112
+ stat = tryStat(_path);
114
113
 
115
114
  if(stat && stat.isFile()) {
116
- return path;
115
+ return _path;
117
116
  }
118
117
  }
119
118
  }