http2-wrapper
Advanced tools
Comparing version 0.8.0 to 1.0.0-alpha.0
{ | ||
"name": "http2-wrapper", | ||
"version": "0.8.0", | ||
"version": "1.0.0-alpha.0", | ||
"description": "HTTP2 client, just with the familiar `https` API", | ||
@@ -10,3 +10,3 @@ "main": "source", | ||
"scripts": { | ||
"test": "xo && nyc ava", | ||
"test": "echo XO $(xo --version) && echo AVA $(ava --version) && xo && nyc ava", | ||
"coveralls": "nyc report --reporter=text-lcov | coveralls" | ||
@@ -34,2 +34,3 @@ }, | ||
"dependencies": { | ||
"quick-lru": "^4.0.1", | ||
"resolve-alpn": "^1.0.0" | ||
@@ -42,9 +43,7 @@ }, | ||
"coveralls": "^3.0.5", | ||
"delay": "^4.3.0", | ||
"create-cert": "^1.0.6", | ||
"get-stream": "^5.1.0", | ||
"nyc": "^14.1.1", | ||
"p-event": "^4.1.0", | ||
"pem": "^1.14.2", | ||
"tempy": "^0.3.0", | ||
"timekeeper": "^2.2.0", | ||
"to-readable-stream": "^2.1.0", | ||
@@ -51,0 +50,0 @@ "xo": "^0.24.0" |
@@ -9,4 +9,2 @@ # http2-wrapper | ||
**This package is under heavy development. It may contain bugs. Don't forget to report them if you notice any.** | ||
This package was created to support HTTP2 without the need to rewrite your code.<br> | ||
@@ -87,3 +85,3 @@ I recommend adapting to the [`http2`](https://nodejs.org/api/http2.html) module if possible - it's much simpler to use and has many cool features! | ||
**Note:** the `session` option accepts an instance of [`Http2Session`](https://nodejs.org/api/http2.html#http2_class_http2session). To pass a SSL session, use `socketSession` instead. | ||
**Note:** the `session` option accepts an instance of [`Http2Session`](https://nodejs.org/api/http2.html#http2_class_http2session). To pass a TLS session, use `tlsSession` instead. | ||
@@ -173,12 +171,6 @@ ### http2.auto(url, options, callback) | ||
An object storing cache for detecting ALPN. It looks like: | ||
An instance of [`quick-lru`](https://github.com/sindresorhus/quick-lru) used for caching ALPN. | ||
```js | ||
{ | ||
'hostname:port:alpn1,alpn2': 'alpn1' | ||
} | ||
``` | ||
There is a maximum of 100 entries. You can modify the limit through `protocolCache.maxSize` - note that the change will be visible globally. | ||
There are maximum 100 entries. | ||
### http2.request(url, options, callback) | ||
@@ -232,3 +224,3 @@ | ||
Type: `number`<br> | ||
Default: `30000` | ||
Default: `60000` | ||
@@ -261,3 +253,3 @@ If there's no activity in given time (milliseconds), the session is closed. | ||
Type: `string` | ||
Type: `string` `URL` `Object` | ||
@@ -278,3 +270,3 @@ Authority used to create a new session. | ||
Returns a new TLS `Socket`. It defaults to `Agent.connect(authority, options)`. | ||
Returns a new `TLSSocket`. It defaults to `Agent.connect(authority, options)`. | ||
@@ -285,3 +277,3 @@ #### agent.closeFreeSessions() | ||
#### agent.destroy() | ||
#### agent.destroy(reason) | ||
@@ -294,2 +286,3 @@ Destroys **all** sessions. | ||
- [HTTP2 sockets cannot be malformed](https://github.com/nodejs/node/blob/cc8250fab86486632fdeb63892be735d7628cd13/lib/internal/http2/core.js#L725), therefore modifying the socket will have no effect. | ||
- HTTP2 is a binary protocol. Headers are sent without any validation. | ||
@@ -300,12 +293,11 @@ ## Benchmarks | ||
Server: H2O 2.2.5 [`h2o.conf`](h2o.conf)<br> | ||
Node: v12.6.0<br> | ||
Version: master<br> | ||
Node: v12.6.0 | ||
``` | ||
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) | ||
http2-wrapper x 11,886 ops/sec ±1.90% (84 runs sampled) | ||
http2-wrapper - preconfigured session x 14,815 ops/sec ±1.58% (87 runs sampled) | ||
http2 x 18,272 ops/sec ±1.76% (80 runs sampled) | ||
http2 - using PassThrough proxies x 15,215 ops/sec ±2.18% (85 runs sampled) | ||
https x 1,613 ops/sec ±4.56% (75 runs sampled) | ||
http x 6,676 ops/sec ±5.17% (78 runs sampled) | ||
Fastest is http2 | ||
@@ -316,13 +308,13 @@ ``` | ||
- 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`. | ||
- It's `1.537x` slower than `http2`. | ||
- It's `1.280x` slower than `http2` with `PassThrough`. | ||
- It's `7.369x` faster than `https`. | ||
- It's `1.780x` faster than `http`. | ||
`http2-wrapper - preconfigured session`: | ||
- 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`. | ||
- It's `1.233x` slower than `http2`. | ||
- It's `1.027x` slower than `http2` with `PassThrough`. | ||
- It's `9.185x` faster than `https`. | ||
- It's `2.219x` faster than `http`. | ||
@@ -329,0 +321,0 @@ ## Related |
@@ -49,3 +49,3 @@ 'use strict'; | ||
const removeSession = (where, name, session) => { | ||
if (where[name]) { | ||
if (Reflect.has(where, name)) { | ||
const index = where[name].indexOf(session); | ||
@@ -92,3 +92,3 @@ | ||
for (const key of nameKeys) { | ||
if (options[key]) { | ||
if (Reflect.has(options, key)) { | ||
if (typeof options[key] === 'object') { | ||
@@ -106,12 +106,12 @@ name += `:${JSON.stringify(options[key])}`; | ||
_processQueue(name) { | ||
const busyLength = this.busySessions[name] ? this.busySessions[name].length : 0; | ||
const busyLength = Reflect.has(this.busySessions, name) ? this.busySessions[name].length : 0; | ||
if (busyLength < this.maxSessions && this.queue[name] && !this.queue[name].completed) { | ||
if (busyLength < this.maxSessions && Reflect.has(this.queue, name) && !this.queue[name].completed) { | ||
this.queue[name].completed = true; | ||
this.queue[name](); | ||
this.queue[name].completed = true; | ||
} | ||
} | ||
async getSession(authority, options, preconnectOnly = false) { | ||
async getSession(authority, options) { | ||
return new Promise((resolve, reject) => { | ||
@@ -121,3 +121,3 @@ const name = this.getName(authority, options); | ||
if (this.freeSessions[name]) { | ||
if (Reflect.has(this.freeSessions, name)) { | ||
resolve(this.freeSessions[name][0]); | ||
@@ -128,19 +128,7 @@ | ||
if (this.queue[name]) { | ||
let listenersLength = this.queue[name].listeners.length; | ||
if (Reflect.has(this.queue, name)) { | ||
// TODO: limit the maximum amount of listeners | ||
this.queue[name].listeners.push(detached); | ||
if (this.queue[name].preconnectOnly) { | ||
listenersLength--; | ||
} | ||
if (listenersLength < this.maxFreeSessions) { | ||
this.queue[name].listeners.push(detached); | ||
if (this.queue[name].listeners.length === this.maxFreeSessions) { | ||
// All seats are taken, remove entry from the queue. | ||
delete this.queue[name]; | ||
} | ||
return; | ||
} | ||
return; | ||
} | ||
@@ -153,3 +141,3 @@ | ||
// Or the entry will be `undefined` if all seats are taken. | ||
if (this.queue[name] && this.queue[name].completed) { | ||
if (Reflect.has(this.queue, name) && this.queue[name].completed) { | ||
delete this.queue[name]; | ||
@@ -161,2 +149,4 @@ } | ||
try { | ||
let receivedSettings = false; | ||
const session = http2.connect(authority, { | ||
@@ -169,3 +159,4 @@ createConnection: this.createConnection, | ||
session.setTimeout(this.timeout, () => { | ||
session.close(); | ||
// `.close()` would wait until all streams all closed | ||
session.destroy(); | ||
}); | ||
@@ -182,2 +173,8 @@ | ||
session.once('close', () => { | ||
if (!receivedSettings) { | ||
for (const listener of listeners) { | ||
listener.reject(new Error('Session closed without receiving a SETTINGS frame')); | ||
} | ||
} | ||
free(); | ||
@@ -192,3 +189,7 @@ | ||
this.freeSessions[name] = [session]; | ||
if (Reflect.has(this.freeSessions, name)) { | ||
this.freeSessions[name].push(session); | ||
} else { | ||
this.freeSessions[name] = [session]; | ||
} | ||
@@ -198,2 +199,4 @@ for (const listener of listeners) { | ||
} | ||
receivedSettings = true; | ||
}); | ||
@@ -212,3 +215,3 @@ | ||
if (this.busySessions[name]) { | ||
if (Reflect.has(this.busySessions, name)) { | ||
this.busySessions[name].push(session); | ||
@@ -222,7 +225,9 @@ } else { | ||
if (--session[kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams) { | ||
session.unref(); | ||
if (session[kCurrentStreamsCount] === 0) { | ||
session.unref(); | ||
} | ||
if (removeSession(this.busySessions, name, session) && !session.destroyed) { | ||
if ((this.freeSessions[name] || []).length < this.maxFreeSessions) { | ||
if (this.freeSessions[name]) { | ||
if (Reflect.has(this.freeSessions, name)) { | ||
this.freeSessions[name].push(session); | ||
@@ -245,2 +250,4 @@ } else { | ||
} | ||
delete this.queue[name]; | ||
} | ||
@@ -250,3 +257,2 @@ }; | ||
this.queue[name].listeners = listeners; | ||
this.queue[name].preconnectOnly = preconnectOnly; | ||
this.queue[name].completed = false; | ||
@@ -276,3 +282,3 @@ this._processQueue(name); | ||
const port = authority.port || 443; | ||
const host = authority.hostname || authority.host || 'localhost'; | ||
const host = authority.hostname || authority.host; | ||
@@ -292,6 +298,6 @@ return tls.connect(port, host, options); | ||
destroy(error) { | ||
destroy(reason) { | ||
for (const busySessions of Object.values(this.busySessions)) { | ||
for (const session of busySessions) { | ||
session.destroy(error); | ||
session.destroy(reason); | ||
} | ||
@@ -302,3 +308,3 @@ } | ||
for (const session of freeSessions) { | ||
session.destroy(error); | ||
session.destroy(reason); | ||
} | ||
@@ -305,0 +311,0 @@ } |
@@ -5,2 +5,3 @@ 'use strict'; | ||
const https = require('https'); | ||
const QuickLRU = require('quick-lru'); | ||
const isCompatible = require('./utils/is-compatible'); | ||
@@ -11,3 +12,3 @@ const Http2ClientRequest = isCompatible ? require('./client-request') : undefined; | ||
const cache = {}; | ||
const cache = new QuickLRU({maxSize: 100}); | ||
@@ -21,13 +22,7 @@ const prepareRequest = async options => { | ||
let alpnProtocol = cache[name]; | ||
let alpnProtocol = cache.get(name); | ||
if (typeof alpnProtocol === 'undefined') { | ||
alpnProtocol = (await httpResolveALPN(options)).alpnProtocol; | ||
cache[name] = alpnProtocol; | ||
const keys = Object.keys(cache); | ||
while (keys.length > 100) { | ||
delete cache[keys.pop()]; | ||
} | ||
cache.set(name, alpnProtocol); | ||
} | ||
@@ -52,3 +47,3 @@ | ||
_defaultAgent: https.globalAgent, | ||
session: options.socketSession | ||
session: options.tlsSession | ||
}; | ||
@@ -55,0 +50,0 @@ |
@@ -25,3 +25,2 @@ 'use strict'; | ||
const kAuthority = Symbol('authority'); | ||
const kAgent = Symbol('agent'); | ||
const kSession = Symbol('session'); | ||
@@ -48,13 +47,29 @@ const kOptions = Symbol('options'); | ||
const defaultPort = options.defaultPort || (this.agent && this.agent.defaultPort); | ||
if (options.agent) { | ||
if (typeof options.agent.request !== 'function') { | ||
throw new ERR_INVALID_ARG_TYPE('options.agent', ['Agent-like Object', 'undefined', 'false'], options.agent); | ||
} | ||
options = { | ||
protocol: 'https:', | ||
port: defaultPort || 443, | ||
preconnect: true, | ||
...options, | ||
host: options.hostname || options.host || 'localhost' | ||
}; | ||
this.agent = options.agent; | ||
} else if (options.session) { | ||
this[kSession] = options.session; | ||
} else if (options.agent === false) { | ||
this.agent = new Agent({maxFreeSessions: 0}); | ||
} else if (options.agent === null || typeof options.agent === 'undefined') { | ||
if (typeof options.createConnection === 'function') { | ||
// This is a workaround - we don't have to create the session on our own. | ||
this.agent = new Agent({maxFreeSessions: 0}); | ||
this.agent.createConnection = options.createConnection; | ||
} else { | ||
this.agent = globalAgent; | ||
} | ||
} | ||
if (options.protocol !== 'https:') { | ||
if (!options.port) { | ||
options.port = options.defaultPort || (this.agent && this.agent.defaultPort) || 443; | ||
} | ||
options.host = options.hostname || options.host || 'localhost'; | ||
if (options.protocol && options.protocol !== 'https:') { | ||
throw new ERR_INVALID_PROTOCOL(options.protocol, 'https:'); | ||
@@ -87,25 +102,10 @@ } | ||
if (options.agent) { | ||
this[kAgent] = options.agent; | ||
} else if (options.session) { | ||
this[kSession] = options.session; | ||
} else if (options.agent === false) { | ||
this[kAgent] = new Agent({maxFreeSessions: 0}); | ||
} else if (options.agent === null || typeof options.agent === 'undefined') { | ||
if (typeof options.createConnection !== 'function') { | ||
this[kAgent] = globalAgent; | ||
} | ||
} else if (typeof options.agent.request !== 'function') { | ||
throw new ERR_INVALID_ARG_TYPE('options.agent', ['Agent-like Object', 'undefined', 'false'], options.agent); | ||
} | ||
options.ALPNProtocols = ['h2']; | ||
options.session = options.socketSession; | ||
options.session = options.tlsSession; | ||
options.path = options.socketPath; | ||
this[kOptions] = options; | ||
this[kAuthority] = options.authority || `https://${options.hostname || options.host}:${options.port}`; | ||
this[kAuthority] = options.authority || new URL(`https://${options.hostname || options.host}:${options.port}`); | ||
if (this[kAgent] && options.preconnect) { | ||
this[kAgent].getSession(this[kAuthority], options, true).catch(() => {}); | ||
if (this.agent && (options.preconnect || typeof options.preconnect === 'undefined')) { | ||
this.agent.getSession(this[kAuthority], options).catch(() => {}); | ||
} | ||
@@ -191,3 +191,3 @@ | ||
flushHeaders() { | ||
if (this[kFlushedHeaders]) { | ||
if (this[kFlushedHeaders] && !this.destroyed && !this.aborted) { | ||
return; | ||
@@ -280,3 +280,3 @@ } | ||
// eslint-disable-next-line promise/prefer-await-to-then | ||
this[kAgent].request(this[kAuthority], this[kOptions], this[kHeaders]).then(onStream, error => { | ||
this.agent.request(this[kAuthority], this[kOptions], this[kHeaders]).then(onStream, error => { | ||
this.emit('error', error); | ||
@@ -283,0 +283,0 @@ }); |
@@ -16,3 +16,11 @@ 'use strict'; | ||
const type = args[0].includes('.') ? 'property' : 'argument'; | ||
return `The "${args[0]}" ${type} must be of type ${args[1]}. Received ${typeof args[2]}`; | ||
let valid = args[1]; | ||
const isManyTypes = Array.isArray(valid); | ||
if (isManyTypes) { | ||
valid = `${valid.slice(0, valid.length - 1).join(', ')} or ${valid[valid.length - 1]}`; | ||
} | ||
return `The "${args[0]}" ${type} must be ${isManyTypes ? 'one of' : 'of'} type ${valid}. Received ${typeof args[2]}`; | ||
}); | ||
@@ -19,0 +27,0 @@ |
@@ -14,3 +14,3 @@ 'use strict'; | ||
port: options.port || 443, | ||
host: options.hostname || options.host || 'localhost', | ||
host: options.hostname || options.host, | ||
path: options.socketPath, | ||
@@ -17,0 +17,0 @@ session: options.socketSession, |
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
33035
11
743
2
318
+ Addedquick-lru@^4.0.1
+ Addedquick-lru@4.0.1(transitive)