ultimate-express 2.0.2 → 2.0.3

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
@@ -189,6 +189,7 @@ In general, basically all features and options are supported. Use [Express 4.x d
189
189
  - ✅ app.engines
190
190
  - ✅ app.on("mount")
191
191
  - ✅ HEAD method
192
+ - ✅ OPTIONS method
192
193
 
193
194
  ### Application settings
194
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-express",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
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": {
@@ -191,6 +191,12 @@ class Application extends Router {
191
191
  if(request._error) {
192
192
  return this._handleError(request._error, null, request, response);
193
193
  }
194
+ if(request._isOptions && request._matchedMethods.size > 0) {
195
+ const allowedMethods = Array.from(request._matchedMethods).join(',');
196
+ response.setHeader('Allow', allowedMethods);
197
+ response.send(allowedMethods);
198
+ return;
199
+ }
194
200
  response.status(404);
195
201
  this._sendErrorPage(request, response, `Cannot ${request.method} ${request.path}`, false);
196
202
  }
package/src/request.js CHANGED
@@ -69,8 +69,11 @@ module.exports = class Request extends Readable {
69
69
  this._opPath = this._opPath.slice(0, -1);
70
70
  }
71
71
  this.method = req.getCaseSensitiveMethod().toUpperCase();
72
+ this._isOptions = this.method === 'OPTIONS';
73
+ this._isHead = this.method === 'HEAD';
72
74
  this.params = {};
73
-
75
+
76
+ this._matchedMethods = new Set();
74
77
  this._gotParams = new Set();
75
78
  this._stack = [];
76
79
  this._paramStack = [];
@@ -252,7 +255,7 @@ module.exports = class Request extends Readable {
252
255
  if(this.#cachedParsedIp !== null) {
253
256
  return this.#cachedParsedIp;
254
257
  }
255
- const finished = !this.res.socket.writable;
258
+ const finished = this.res.finished;
256
259
  if(finished) {
257
260
  // mark app as one that needs ip after response
258
261
  this.app.needsIpAfterResponse = true;
package/src/response.js CHANGED
@@ -69,6 +69,7 @@ class Socket extends EventEmitter {
69
69
 
70
70
  module.exports = class Response extends Writable {
71
71
  #socket = null;
72
+ #ended = false;
72
73
  #pendingChunks = [];
73
74
  #lastWriteChunkTime = 0;
74
75
  #writeTimeout = null;
@@ -112,13 +113,16 @@ module.exports = class Response extends Writable {
112
113
  this._res.cork(() => {
113
114
  this._res.close();
114
115
  this.finished = true;
115
- if(this.socketExists) this.socket.emit('close');
116
+ this.#socket?.emit('close');
116
117
  });
117
118
  });
119
+ this.once('close', () => {
120
+ this.#ended = true
121
+ })
118
122
  }
119
123
 
120
124
  get socket() {
121
- this.socketExists = true;
125
+ if(this.#ended) return null;
122
126
  if(!this.#socket) {
123
127
  this.#socket = new Socket(this);
124
128
  }
@@ -188,7 +192,7 @@ module.exports = class Response extends Writable {
188
192
  super.end();
189
193
  this.finished = true;
190
194
  this.writingChunk = false;
191
- if (this.socketExists) this.socket.emit('close');
195
+ this.#socket?.emit('close');
192
196
  callback(null);
193
197
  } else if (!ok) {
194
198
  this._res.ab = chunk;
@@ -198,7 +202,7 @@ module.exports = class Response extends Writable {
198
202
  const [ok, done] = this._res.tryEnd(this._res.ab.slice(offset - this._res.abOffset), this.totalSize);
199
203
  if (done) {
200
204
  this.finished = true;
201
- if (this.socketExists) this.socket.emit('close');
205
+ this.#socket?.emit('close');
202
206
  }
203
207
  if (ok) {
204
208
  this.writingChunk = false;
@@ -258,10 +262,18 @@ module.exports = class Response extends Writable {
258
262
  sendStatus(code) {
259
263
  return this.status(code).send(statuses.message[+code] ?? code.toString());
260
264
  }
261
- end(data) {
265
+ end(data, cb) {
266
+ if(typeof data === 'function') {
267
+ cb = data;
268
+ data = undefined;
269
+ }
270
+ if(typeof cb !== 'function') {
271
+ cb = undefined; // silence the error?
272
+ }
273
+
262
274
  if(this.writingChunk) {
263
275
  this.once('drain', () => {
264
- this.end(data);
276
+ this.end(data, cb);
265
277
  });
266
278
  return;
267
279
  }
@@ -282,9 +294,13 @@ module.exports = class Response extends Writable {
282
294
  if(fresh) {
283
295
  this._res.end();
284
296
  this.finished = true;
285
- if(this.socketExists) this.socket.emit('close');
297
+ this.#socket?.emit('close');
286
298
  this.emit('finish');
287
299
  this.emit('close');
300
+ cb && queueMicrotask(() => {
301
+ this.#ended = true;
302
+ cb();
303
+ });
288
304
  return;
289
305
  }
290
306
  }
@@ -309,9 +325,13 @@ module.exports = class Response extends Writable {
309
325
  }
310
326
 
311
327
  this.finished = true;
312
- if(this.socketExists) this.socket.emit('close');
328
+ this.#socket?.emit('close');
313
329
  this.emit('finish');
314
330
  this.emit('close');
331
+ cb && queueMicrotask(() => {
332
+ this.#ended = true;
333
+ cb();
334
+ });
315
335
  });
316
336
  return this;
317
337
  }
package/src/router.js CHANGED
@@ -152,6 +152,9 @@ module.exports = class Router extends EventEmitter {
152
152
  all: method === 'ALL' || method === 'USE',
153
153
  gettable: method === 'GET' || method === 'HEAD',
154
154
  };
155
+ if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*') || (route.path.includes('(') && route.path.includes(')'))) && route.pattern instanceof RegExp) {
156
+ route.complex = true;
157
+ }
155
158
  routes.push(route);
156
159
  // normal routes optimization
157
160
  if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
@@ -296,6 +299,12 @@ module.exports = class Router extends EventEmitter {
296
299
  if(request._error) {
297
300
  return this._handleError(request._error, null, request, response);
298
301
  }
302
+ if(request._isOptions && request._matchedMethods.size > 0) {
303
+ const allowedMethods = Array.from(request._matchedMethods).join(',');
304
+ response.setHeader('Allow', allowedMethods);
305
+ response.send(allowedMethods);
306
+ return;
307
+ }
299
308
  response.status(404);
300
309
  request.noEtag = true;
301
310
  this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
@@ -369,7 +378,7 @@ module.exports = class Router extends EventEmitter {
369
378
  req.route = route;
370
379
  if(route.optimizedParams) {
371
380
  req.params = {...req.optimizedParams};
372
- } else if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*') || (route.path.includes('(') && route.path.includes(')'))) && route.pattern instanceof RegExp) {
381
+ } else if(route.complex) {
373
382
  let path = req._originalPath;
374
383
  if(req._stack.length > 0) {
375
384
  path = path.replace(this.getFullMountpath(req), '');
@@ -445,7 +454,7 @@ module.exports = class Router extends EventEmitter {
445
454
  }
446
455
 
447
456
  async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
448
- let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || (r.gettable && req.method === 'HEAD')) && this._pathMatches(r, req), startIndex);
457
+ let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) && this._pathMatches(r, req), startIndex);
449
458
  const route = routes[routeIndex];
450
459
  if(!route) {
451
460
  if(!skipCheck) {
@@ -487,6 +496,9 @@ module.exports = class Router extends EventEmitter {
487
496
  if(thingamabob) {
488
497
  if(thingamabob === 'route' || thingamabob === 'skipPop') {
489
498
  if(route.use && thingamabob !== 'skipPop') {
499
+ if(req._isOptions) {
500
+ return resolve(false);
501
+ }
490
502
  req._stack.pop();
491
503
 
492
504
  req._opPath = req._stack.length > 0 ? req._originalPath.replace(this.getFullMountpath(req), '') : req._originalPath;
@@ -546,6 +558,15 @@ module.exports = class Router extends EventEmitter {
546
558
  }
547
559
 
548
560
  try {
561
+ // handling OPTIONS method
562
+ if(req._isOptions && !route.all && route.method !== 'OPTIONS') {
563
+ req._matchedMethods.add(route.method);
564
+ if(route.gettable) {
565
+ req._matchedMethods.add('HEAD');
566
+ }
567
+ return next();
568
+ }
569
+
549
570
  // skipping routes we already went through via optimized path
550
571
  if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
551
572
  return next();