http2-wrapper
Advanced tools
Comparing version 0.5.0 to 0.5.1
{ | ||
"name": "http2-wrapper", | ||
"version": "0.5.0", | ||
"version": "0.5.1", | ||
"description": "HTTP2 client, just with the familiar `https` API", | ||
@@ -5,0 +5,0 @@ "main": "source", |
@@ -156,2 +156,12 @@ # http2-wrapper | ||
### http2.auto.prepareRequest(url, options) | ||
Performs [ALPN](https://nodejs.org/api/tls.html#tls_alpn_and_sni) negotiation. | ||
Returns a Promise giving an object with `options` and `request`. | ||
Depending on the ALPN protocol, `request` is either `http2.request` or `http.request`.<br> | ||
Options are normalized. | ||
**Note**: the `agent` option also accepts an object with `http`, `https` and `http2` properties. | ||
### http2.auto.resolveALPN(options) | ||
@@ -161,2 +171,14 @@ | ||
### http2.auto.protocolCache | ||
An object storing cache for detecting ALPN. It looks like: | ||
```js | ||
{ | ||
'hostname:port:alpn1,alpn2': 'alpn1' | ||
} | ||
``` | ||
There are maximum 100 entries. | ||
### http2.request(url, options, callback) | ||
@@ -234,4 +256,3 @@ | ||
Returns a Promise giving free `Http2Session`. If no free sessions are found, a new one is created.<br> | ||
You can cancel the session query by calling `.cancel()` on the promise. | ||
Returns a Promise giving free `Http2Session`. If no free sessions are found, a new one is created. | ||
@@ -252,4 +273,3 @@ ##### authority | ||
Returns a Promise giving `Http2Stream`.<br> | ||
You can cancel the request by calling `.cancel()` on the promise, as well as using `.abort()` on the `ClientRequest` instance. | ||
Returns a Promise giving `Http2Stream`. | ||
@@ -278,11 +298,11 @@ #### agent.createConnection(authority, options) | ||
Node: v12.6.0<br> | ||
Commit: latest<br> | ||
Version: master<br> | ||
``` | ||
http2-wrapper x 9,453 ops/sec ±6.98% (75 runs sampled) | ||
http2-wrapper - preconfigured session x 11,955 ops/sec ±1.89% (84 runs sampled) | ||
http2 x 18,478 ops/sec ±1.36% (84 runs sampled) | ||
http2 - using PassThrough x 16,099 ops/sec ±1.60% (84 runs sampled) | ||
https x 1,547 ops/sec ±3.93% (74 runs sampled) | ||
http x 6,138 ops/sec ±5.15% (74 runs sampled) | ||
http2-wrapper x 10,839 ops/sec ±2.19% (84 runs sampled) | ||
http2-wrapper - preconfigured session x 13,005 ops/sec ±3.65% (84 runs sampled) | ||
http2 x 17,100 ops/sec ±3.40% (81 runs sampled) | ||
http2 - using PassThrough proxies x 14,831 ops/sec ±2.22% (84 runs sampled) | ||
https x 1,623 ops/sec ±3.40% (76 runs sampled) | ||
http x 6,761 ops/sec ±3.46% (76 runs sampled) | ||
Fastest is http2 | ||
@@ -293,13 +313,13 @@ ``` | ||
- It's `1.95x` slower than `http2`. | ||
- It's `1.70x` slower than `http2` with `PassThrough`. | ||
- It's `6.11x` faster than `https`. | ||
- It's `1.54x` faster than `http`. | ||
- It's `1.58x` slower than `http2`. | ||
- It's `1.37x` slower than `http2` with `PassThrough`. | ||
- It's `6.68x` faster than `https`. | ||
- It's `1.60x` faster than `http`. | ||
`http2-wrapper - preconfigured session`: | ||
- It's `1.55x` slower than `http2`. | ||
- It's `1.35x` slower than `http2` with `PassThrough`. | ||
- It's `7.73x` faster than `https`. | ||
- It's `1.95x` faster than `http`. | ||
- It's `1.31x` slower than `http2`. | ||
- It's `1.14x` slower than `http2` with `PassThrough`. | ||
- It's `8.01x` faster than `https`. | ||
- It's `1.92x` faster than `http`. | ||
@@ -306,0 +326,0 @@ ## Related |
@@ -66,3 +66,3 @@ 'use strict'; | ||
class Agent extends EventEmitter { | ||
constructor({timeout = 30000, maxSessions = Infinity, maxFreeSessions = 1} = {}) { | ||
constructor({timeout = 60000, maxSessions = Infinity, maxFreeSessions = 1} = {}) { | ||
super(); | ||
@@ -106,13 +106,11 @@ | ||
if (busyLength < this.maxSessions && this.queue[name]) { | ||
if (busyLength < this.maxSessions && this.queue[name] && !this.queue[name].completed) { | ||
this.queue[name](); | ||
delete this.queue[name]; | ||
this.queue[name].completed = true; | ||
} | ||
} | ||
getSession(authority, options) { | ||
let onCancel = () => {}; | ||
const promise = new Promise((resolve, reject) => { | ||
async getSession(authority, options, preconnectOnly = false) { | ||
return new Promise((resolve, reject) => { | ||
const name = this.getName(authority, options); | ||
@@ -123,126 +121,129 @@ const detached = {resolve, reject}; | ||
resolve(this.freeSessions[name][0]); | ||
} else if (this.queue[name]) { | ||
this.queue[name].listeners.push(detached); | ||
} else { | ||
const listeners = [detached]; | ||
this.queue[name] = () => { | ||
try { | ||
const session = http2.connect(authority, { | ||
createConnection: this.createConnection, | ||
...options | ||
}); | ||
session[kCurrentStreamsCount] = 0; | ||
return; | ||
} | ||
session.setTimeout(this.timeout, () => { | ||
session.close(); | ||
}); | ||
if (this.queue[name]) { | ||
let listenersLength = this.queue[name].listeners.length; | ||
session.once('error', error => { | ||
session.destroy(); | ||
if (this.queue[name].preconnectOnly) { | ||
listenersLength--; | ||
} | ||
reject(error); | ||
}); | ||
if (listenersLength < this.maxFreeSessions) { | ||
this.queue[name].listeners.push(detached); | ||
session.once('close', () => { | ||
removeSession(this.freeSessions, name, session); | ||
this._processQueue(name); | ||
}); | ||
session.once('remoteSettings', () => { | ||
this.freeSessions[name] = [session]; | ||
for (const listener of listeners) { | ||
listener.resolve(session); | ||
} | ||
}); | ||
session[kRequest] = session.request; | ||
session.request = () => { | ||
throw new Error('Invalid usage. Use `agent.request(authority, options, headers)` instead.'); | ||
}; | ||
} catch (error) { | ||
for (const listener of listeners) { | ||
listener.reject(error); | ||
} | ||
if (this.queue[name].listeners.length === this.maxFreeSessions) { | ||
// All seats are taken, remove entry from the queue. | ||
delete this.queue[name]; | ||
} | ||
}; | ||
this.queue[name].listeners = listeners; | ||
this._processQueue(name); | ||
return; | ||
} | ||
} | ||
onCancel = () => { | ||
if (this.queue[name]) { | ||
const {listeners} = this.queue[name]; | ||
this.queue[name].splice(listeners.indexOf(detached), 1); | ||
const listeners = [detached]; | ||
if (listeners.length === 0) { | ||
delete this.queue[name]; | ||
} | ||
const free = () => { | ||
// If our entry is replaced,`completed` will be `false`. | ||
// Or the entry will be `undefined` if all seats are taken. | ||
if (this.queue[name] && this.queue[name].completed) { | ||
delete this.queue[name]; | ||
} | ||
}; | ||
}); | ||
promise.cancel = () => onCancel(); | ||
this.queue[name] = () => { | ||
try { | ||
const session = http2.connect(authority, { | ||
createConnection: this.createConnection, | ||
...options | ||
}); | ||
session[kCurrentStreamsCount] = 0; | ||
return promise; | ||
} | ||
session.setTimeout(this.timeout, () => { | ||
session.close(); | ||
}); | ||
request(authority, options, headers) { | ||
let onCancel = () => {}; | ||
session.once('error', error => { | ||
session.destroy(); | ||
const promise = (async () => { | ||
const name = this.getName(authority, options); | ||
const sessionPromise = this.getSession(authority, options); | ||
for (const listener of listeners) { | ||
listener.reject(error); | ||
} | ||
}); | ||
onCancel = () => { | ||
sessionPromise.cancel(); | ||
}; | ||
session.once('close', () => { | ||
free(); | ||
const session = await sessionPromise; | ||
const stream = session[kRequest](headers, { | ||
endStream: false | ||
}); | ||
removeSession(this.freeSessions, name, session); | ||
this._processQueue(name); | ||
}); | ||
session.ref(); | ||
session.once('remoteSettings', () => { | ||
free(); | ||
if (++session[kCurrentStreamsCount] >= session.remoteSettings.maxConcurrentStreams) { | ||
removeSession(this.freeSessions, name, session); | ||
this.freeSessions[name] = [session]; | ||
if (this.busySessions[name]) { | ||
this.busySessions[name].push(session); | ||
} else { | ||
this.busySessions[name] = [session]; | ||
} | ||
} | ||
for (const listener of listeners) { | ||
listener.resolve(session); | ||
} | ||
}); | ||
stream.once('close', () => { | ||
if (--session[kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams) { | ||
session.unref(); | ||
session[kRequest] = session.request; | ||
session.request = headers => { | ||
const stream = session[kRequest](headers, { | ||
endStream: false | ||
}); | ||
if (removeSession(this.busySessions, name, session) && !session.destroyed) { | ||
if ((this.freeSessions[name] || []).length < this.maxFreeSessions) { | ||
if (this.freeSessions[name]) { | ||
this.freeSessions[name].push(session); | ||
session.ref(); | ||
if (++session[kCurrentStreamsCount] >= session.remoteSettings.maxConcurrentStreams) { | ||
removeSession(this.freeSessions, name, session); | ||
if (this.busySessions[name]) { | ||
this.busySessions[name].push(session); | ||
} else { | ||
this.freeSessions[name] = [session]; | ||
this.busySessions[name] = [session]; | ||
} | ||
} else { | ||
session.close(); | ||
} | ||
stream.once('close', () => { | ||
if (--session[kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams) { | ||
session.unref(); | ||
if (removeSession(this.busySessions, name, session) && !session.destroyed) { | ||
if ((this.freeSessions[name] || []).length < this.maxFreeSessions) { | ||
if (this.freeSessions[name]) { | ||
this.freeSessions[name].push(session); | ||
} else { | ||
this.freeSessions[name] = [session]; | ||
} | ||
} else { | ||
session.close(); | ||
} | ||
} | ||
} | ||
}); | ||
return stream; | ||
}; | ||
} catch (error) { | ||
for (const listener of listeners) { | ||
listener.reject(error); | ||
} | ||
} | ||
}); | ||
onCancel = () => { | ||
stream.close(http2.constants.NGHTTP2_CANCEL); | ||
}; | ||
return stream; | ||
})(); | ||
this.queue[name].listeners = listeners; | ||
this.queue[name].preconnectOnly = preconnectOnly; | ||
this.queue[name].completed = false; | ||
this._processQueue(name); | ||
}); | ||
} | ||
promise.cancel = () => onCancel(); | ||
async request(authority, options, headers) { | ||
const session = await this.getSession(authority, options); | ||
const stream = session.request(headers); | ||
return promise; | ||
return stream; | ||
} | ||
@@ -249,0 +250,0 @@ |
@@ -10,3 +10,3 @@ 'use strict'; | ||
module.exports = async (input, options, callback) => { | ||
const prepareRequest = async (input, options) => { | ||
if (typeof input === 'string' || input instanceof URL) { | ||
@@ -35,4 +35,4 @@ input = urlToOptions(new URL(input)); | ||
const keys = Object.keys(cache); | ||
/* istanbul ignore next */ | ||
if (keys.length > 100) { | ||
while (keys.length > 100) { | ||
delete cache[keys.pop()]; | ||
@@ -47,3 +47,6 @@ } | ||
return new Http2ClientRequest(options, callback); | ||
return { | ||
options, | ||
request: Http2ClientRequest.request | ||
}; | ||
} | ||
@@ -61,6 +64,17 @@ | ||
options.session = options.socketSession; | ||
return new http.ClientRequest(options, callback); | ||
return { | ||
options, | ||
request: http.request | ||
}; | ||
}; | ||
module.exports = async (input, options, callback) => { | ||
const {options: preparedOptions, request} = await prepareRequest(input, options); | ||
return request(preparedOptions, callback); | ||
}; | ||
module.exports.resolveALPN = httpResolveALPN; | ||
module.exports.prepareRequest = prepareRequest; | ||
module.exports.protocolCache = cache; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const {Agent, globalAgent} = require('./agent'); | ||
const HTTP2IncomingMessage = require('./incoming-message'); | ||
const IncomingMessage = require('./incoming-message'); | ||
const urlToOptions = require('./utils/url-to-options'); | ||
@@ -29,6 +29,5 @@ const proxyEvents = require('./utils/proxy-events'); | ||
const kFlushedHeaders = Symbol('flushedHeaders'); | ||
const kPendingPromise = Symbol('pendingPromise'); | ||
class ClientRequest extends Writable { | ||
constructor(input, options, cb) { | ||
constructor(input, options, callback) { | ||
super(); | ||
@@ -41,7 +40,7 @@ | ||
if (typeof options === 'function') { | ||
// (options, cb) | ||
cb = options; | ||
// (options, callback) | ||
callback = options; | ||
options = input; | ||
} else { | ||
// (input, options, cb) | ||
// (input, options, callback) | ||
options = {...input, ...options}; | ||
@@ -72,4 +71,4 @@ } | ||
this.method = options.method ? options.method.toUpperCase() : 'GET'; | ||
this.path = options.path || '/'; | ||
this.method = options.method; | ||
this.path = options.path; | ||
@@ -111,4 +110,3 @@ this.res = null; | ||
if (this[kAgent] && options.preconnect) { | ||
this[kPendingPromise] = this[kAgent].getSession(this[kAuthority], options); | ||
this[kPendingPromise].catch(() => {}); | ||
this[kAgent].getSession(this[kAuthority], options, true).catch(() => {}); | ||
} | ||
@@ -120,4 +118,4 @@ | ||
if (cb) { | ||
this.once('response', cb); | ||
if (callback) { | ||
this.once('response', callback); | ||
} | ||
@@ -128,2 +126,22 @@ | ||
set method(value) { | ||
if (value) { | ||
this[kHeaders][HTTP2_HEADER_METHOD] = value.toUpperCase(); | ||
} | ||
} | ||
get method() { | ||
return this[kHeaders][HTTP2_HEADER_METHOD]; | ||
} | ||
set path(value) { | ||
if (value) { | ||
this[kHeaders][HTTP2_HEADER_PATH] = value; | ||
} | ||
} | ||
get path() { | ||
return this[kHeaders][HTTP2_HEADER_PATH]; | ||
} | ||
_write(chunk, encoding, callback) { | ||
@@ -163,4 +181,2 @@ this.flushHeaders(); | ||
this._request.close(NGHTTP2_CANCEL); | ||
} else if (this[kPendingPromise]) { | ||
this[kPendingPromise].cancel(); | ||
} | ||
@@ -186,12 +202,2 @@ } | ||
const headers = { | ||
...this[kHeaders], | ||
[HTTP2_HEADER_METHOD]: this.method | ||
}; | ||
// Must not specify the `:path` and `:scheme` headers for `CONNECT` requests. | ||
if (!isConnectMethod) { | ||
headers[HTTP2_HEADER_PATH] = this.path; | ||
} | ||
// The real magic is here | ||
@@ -209,7 +215,7 @@ const onStream = stream => { | ||
this._request.once('response', (headers, flags, rawHeaders) => { | ||
this.res = new HTTP2IncomingMessage(this.socket); | ||
this.res = new IncomingMessage(this.socket); | ||
this.res.req = this; | ||
this.res.statusCode = headers[HTTP2_HEADER_STATUS]; | ||
this.res.headers = {...headers}; | ||
this.res.rawHeaders = [...rawHeaders]; | ||
this.res.headers = headers; | ||
this.res.rawHeaders = rawHeaders; | ||
@@ -252,4 +258,4 @@ this.res.once('end', () => { | ||
// Assigns trailers to the response object. | ||
this.res.trailers = {...trailers}; | ||
this.res.rawTrailers = [...rawTrailers]; | ||
this.res.trailers = trailers; | ||
this.res.rawTrailers = rawTrailers; | ||
}); | ||
@@ -271,3 +277,3 @@ | ||
try { | ||
onStream(this[kSession].request(headers, { | ||
onStream(this[kSession].request(this[kHeaders], { | ||
endStream: false | ||
@@ -279,6 +285,4 @@ })); | ||
} else { | ||
this[kPendingPromise] = this[kAgent].request(this[kAuthority], this[kOptions], headers); | ||
// eslint-disable-next-line promise/prefer-await-to-then | ||
this[kPendingPromise].then(onStream, error => { | ||
this[kAgent].request(this[kAuthority], this[kOptions], this[kHeaders]).then(onStream, error => { | ||
this.emit('error', error); | ||
@@ -321,8 +325,8 @@ }); | ||
setTimeout(ms, cb) { | ||
setTimeout(ms, callback) { | ||
if (this._request) { | ||
this._request.setTimeout(ms, cb); | ||
this._request.setTimeout(ms, callback); | ||
} else { | ||
this.once('socket', () => { | ||
this._request.setTimeout(ms, cb); | ||
this._request.setTimeout(ms, callback); | ||
}); | ||
@@ -347,2 +351,7 @@ } | ||
const request = (url, options, callback) => { | ||
return new ClientRequest(url, options, callback); | ||
}; | ||
module.exports = ClientRequest; | ||
module.exports.request = request; |
@@ -8,8 +8,4 @@ 'use strict'; | ||
const request = (url, options, cb) => { | ||
return new ClientRequest(url, options, cb); | ||
}; | ||
const get = (url, options, cb) => { | ||
const req = request(url, options, cb); | ||
const get = (url, options, callback) => { | ||
const req = ClientRequest.request(url, options, callback); | ||
req.end(); | ||
@@ -24,3 +20,3 @@ | ||
auto, | ||
request, | ||
request: ClientRequest.request, | ||
get, | ||
@@ -27,0 +23,0 @@ ClientRequest, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
31596
711
327
6