Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

http2-wrapper

Package Overview
Dependencies
Maintainers
1
Versions
59
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

http2-wrapper - npm Package Compare versions

Comparing version 1.0.0-beta.4.8 to 1.0.0-beta.5.0

6

package.json
{
"name": "http2-wrapper",
"version": "1.0.0-beta.4.8",
"version": "1.0.0-beta.5.0",
"description": "HTTP2 client, just with the familiar `https` API",

@@ -10,3 +10,3 @@ "main": "source",

"scripts": {
"test": "xo && nyc --reporter=lcovonly --reporter=text ava"
"test": "xo && nyc --reporter=lcovonly --reporter=text --reporter=html ava"
},

@@ -42,3 +42,3 @@ "files": [

"get-stream": "^5.1.0",
"got": "^11.0.2",
"got": "^11.4.0",
"lolex": "^6.0.0",

@@ -45,0 +45,0 @@ "many-keys-map": "^1.0.2",

@@ -5,3 +5,3 @@ # http2-wrapper

[![Node CI](https://github.com/szmarczak/http2-wrapper/workflows/Node%20CI/badge.svg)](https://github.com/szmarczak/http2-wrapper/actions)
[![Coverage Status](https://coveralls.io/repos/github/szmarczak/http2-wrapper/badge.svg?branch=master)](https://coveralls.io/github/szmarczak/http2-wrapper?branch=master)
[![codecov](https://codecov.io/gh/szmarczak/http2-wrapper/branch/master/graph/badge.svg)](https://codecov.io/gh/szmarczak/http2-wrapper)
[![npm](https://img.shields.io/npm/dm/http2-wrapper.svg)](https://www.npmjs.com/package/http2-wrapper)

@@ -225,3 +225,3 @@ [![install size](https://packagephobia.now.sh/badge?p=http2-wrapper)](https://packagephobia.now.sh/result?p=http2-wrapper)

The maximum amount of sessions per origin.
The maximum amount of sessions in total.

@@ -231,6 +231,8 @@ ##### maxFreeSessions

Type: `number`<br>
Default: `1`
Default: `10`
The maximum amount of free sessions per origin.
The maximum amount of free sessions in total. This only applies to sessions with no pending requests.
**Note:** It is possible that the amount will be exceeded when sessions have at least 1 pending request.
##### maxCachedTlsSessions

@@ -328,3 +330,4 @@

Server: H2O v2.2.5 [`h2o.conf`](h2o.conf)<br>
Node: v13.8.0
Node: v14.5.0
Linux: 5.6.18-156.current

@@ -334,10 +337,10 @@ `auto` means `http2wrapper.auto`.

```
http2-wrapper x 12,417 ops/sec ±3.72% (83 runs sampled)
http2-wrapper - preconfigured session x 14,517 ops/sec ±1.39% (83 runs sampled)
http2-wrapper - auto x 11,373 ops/sec ±3.17% (84 runs sampled)
http2 x 16,172 ops/sec ±1.21% (85 runs sampled)
https - auto - keepalive x 13,251 ops/sec ±3.84% (79 runs sampled)
https - keepalive x 13,158 ops/sec ±2.88% (78 runs sampled)
https x 1,618 ops/sec ±2.07% (82 runs sampled)
http x 5,922 ops/sec ±2.87% (79 runs sampled)
http2-wrapper x 12,181 ops/sec ±3.39% (75 runs sampled)
http2-wrapper - preconfigured session x 13,140 ops/sec ±2.51% (79 runs sampled)
http2-wrapper - auto x 11,412 ops/sec ±2.55% (78 runs sampled)
http2 x 16,050 ops/sec ±1.39% (86 runs sampled)
https - auto - keepalive x 12,288 ops/sec ±2.69% (79 runs sampled)
https - keepalive x 12,155 ops/sec ±3.32% (78 runs sampled)
https x 1,604 ops/sec ±2.03% (77 runs sampled)
http x 6,041 ops/sec ±3.82% (76 runs sampled)
Fastest is http2

@@ -347,20 +350,20 @@ ```

`http2-wrapper`:
- 23% less performant than `http2`
- 6% less performant than `https - keepalive`
- 110% more performant than `http`
- 32% **less** performant than `http2`
- as performant as `https - keepalive`
- 100% **more** performant than `http`
`http2-wrapper - preconfigured session`:
- 10% less performant than `http2`
- 10% more performant than `https - keepalive`
- 145% more performant than `http`
- 22% **less** performant than `http2`
- 8% **more** performant than `https - keepalive`
- 118% **more** performant than `http`
`http2-wrapper - auto`:
- 30% less performant than `http2`
- 14% less performant than `https - keepalive`
- 92% more performant than `http`
- 41% **less** performant than `http2`
- 8% **less** performant than `https - keepalive`
- 89% **more** performant than `http`
`https - auto - keepalive`:
- 18% less performant than `http2`
- 31% **less** performant than `http2`
- as performant as `https - keepalive`
- 124% more performant than `http`
- 103% **more** performant than `http`

@@ -367,0 +370,0 @@ ## Related

@@ -10,2 +10,3 @@ 'use strict';

const kOriginSet = Symbol('cachedOriginSet');
const kGracefullyClosing = Symbol('gracefullyClosing');

@@ -47,48 +48,31 @@ const nameKeys = [

const removeSession = (where, name, session) => {
if (name in where) {
const index = where[name].indexOf(session);
const getSortedIndex = (array, value, compare) => {
let low = 0;
let high = array.length;
if (index !== -1) {
where[name].splice(index, 1);
while (low < high) {
const mid = (low + high) >>> 1;
if (where[name].length === 0) {
delete where[name];
}
return true;
/* istanbul ignore next */
if (compare(array[mid], value)) {
// This never gets called because we use descending sort. Better to have this anyway.
low = mid + 1;
} else {
high = mid;
}
}
return false;
return low;
};
const addSession = (where, name, session) => {
if (name in where) {
where[name].push(session);
} else {
where[name] = [session];
}
const compareSessions = (a, b) => {
return a.remoteSettings.maxConcurrentStreams > b.remoteSettings.maxConcurrentStreams;
};
const getSessions = (where, name, normalizedOrigin) => {
if (!(name in where)) {
return [];
}
return where[name].filter(session => {
return !session.closed && !session.destroyed && session[kOriginSet].includes(normalizedOrigin);
});
};
// See https://tools.ietf.org/html/rfc8336
const closeCoveredSessions = (where, name, session) => {
if (!(name in where)) {
return;
}
const closeCoveredSessions = (where, session) => {
// Clients SHOULD NOT emit new requests on any connection whose Origin
// Set is a proper subset of another connection's Origin Set, and they
// SHOULD close it once all outstanding requests are satisfied.
for (const coveredSession of where[name]) {
for (const coveredSession of where) {
if (

@@ -102,7 +86,6 @@ // The set is a proper subset when its length is less than the other set.

// Makes sure that the session can handle all requests from the covered session.
// TODO: can the session become uncovered when a stream is closed after checking this condition?
coveredSession[kCurrentStreamsCount] + session[kCurrentStreamsCount] <= session.remoteSettings.maxConcurrentStreams
) {
// This allows pending requests to finish and prevents making new requests.
coveredSession.close();
gracefullyClose(coveredSession);
}

@@ -113,8 +96,4 @@ }

// This is basically inverted `closeCoveredSessions(...)`.
const closeSessionIfCovered = (where, name, coveredSession) => {
if (!(name in where)) {
return;
}
for (const session of where[name]) {
const closeSessionIfCovered = (where, coveredSession) => {
for (const session of where) {
if (

@@ -125,3 +104,3 @@ coveredSession[kOriginSet].length < session[kOriginSet].length &&

) {
coveredSession.close();
gracefullyClose(coveredSession);
}

@@ -131,4 +110,33 @@ }

const getSessions = ({agent, isFree}) => {
const result = {};
// eslint-disable-next-line guard-for-in
for (const normalizedOptions in agent.sessions) {
const sessions = agent.sessions[normalizedOptions];
const filtered = sessions.filter(session => {
const result = session[Agent.kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams;
return isFree ? result : !result;
});
if (filtered.length !== 0) {
result[normalizedOptions] = filtered;
}
}
return result;
};
const gracefullyClose = session => {
session[kGracefullyClosing] = true;
if (session[kCurrentStreamsCount] === 0) {
session.close();
}
};
class Agent extends EventEmitter {
constructor({timeout = 60000, maxSessions = Infinity, maxFreeSessions = 1, maxCachedTlsSessions = 100} = {}) {
constructor({timeout = 60000, maxSessions = Infinity, maxFreeSessions = 10, maxCachedTlsSessions = 100} = {}) {
super();

@@ -138,8 +146,9 @@

// is equal to or greater than the `maxConcurrentStreams` value.
this.busySessions = {};
// A session is considered free when its current streams count
// is less than the `maxConcurrentStreams` value.
this.freeSessions = {};
// SESSIONS[NORMALIZED_OPTIONS] = [];
this.sessions = {};
// The queue for creating new sessions. It looks like this:

@@ -157,10 +166,12 @@ // QUEUE[NORMALIZED_OPTIONS][NORMALIZED_ORIGIN] = ENTRY_FUNCTION

// Max sessions per origin.
// Max sessions in total
this.maxSessions = maxSessions;
// Max free sessions per origin.
// Max free sessions in total
// TODO: decreasing `maxFreeSessions` should close some sessions
// TODO: should `maxFreeSessions` be related only to sessions with 0 pending streams?
this.maxFreeSessions = maxFreeSessions;
this._freeSessionsCount = 0;
this._sessionsCount = 0;
// We don't support push streams by default.

@@ -206,8 +217,9 @@ this.settings = {

// We need the busy sessions length to check if a session can be created.
const busyLength = getSessions(this.busySessions, normalizedOptions, normalizedOrigin).length;
const item = this.queue[normalizedOptions][normalizedOrigin];
// The entry function can be run only once.
if (busyLength < this.maxSessions && !item.completed) {
// BUG: The session may be never created when:
// - the first condition is false AND
// - this function is never called with the same arguments in the future.
if (this._sessionsCount < this.maxSessions && !item.completed) {
item.completed = true;

@@ -219,7 +231,2 @@

_closeCoveredSessions(normalizedOptions, session) {
closeCoveredSessions(this.freeSessions, normalizedOptions, session);
closeCoveredSessions(this.busySessions, normalizedOptions, session);
}
getSession(origin, options, listeners) {

@@ -248,24 +255,61 @@ return new Promise((resolve, reject) => {

if (normalizedOptions in this.freeSessions) {
// Look for all available free sessions.
const freeSessions = getSessions(this.freeSessions, normalizedOptions, normalizedOrigin);
if (normalizedOptions in this.sessions) {
const sessions = this.sessions[normalizedOptions];
if (freeSessions.length !== 0) {
// Use session which has the biggest stream capacity in order to use the smallest number of sessions possible.
const session = freeSessions.reduce((previousSession, nextSession) => {
let maxConcurrentStreams = -1;
let currentStreamsCount = -1;
let optimalSession;
// We could just do this.sessions[normalizedOptions].find(...) but that isn't optimal.
// Additionally, we are looking for session which has biggest current pending streams count.
for (const session of sessions) {
const sessionMaxConcurrentStreams = session.remoteSettings.maxConcurrentStreams;
if (sessionMaxConcurrentStreams < maxConcurrentStreams) {
break;
}
if (session[kOriginSet].includes(normalizedOrigin)) {
const sessionCurrentStreamsCount = session[kCurrentStreamsCount];
if (
nextSession.remoteSettings.maxConcurrentStreams >= previousSession.remoteSettings.maxConcurrentStreams &&
nextSession[kCurrentStreamsCount] > previousSession[kCurrentStreamsCount]
sessionCurrentStreamsCount >= sessionMaxConcurrentStreams ||
session[kGracefullyClosing] ||
// Unfortunately the `close` event isn't called immediately,
// so `session.destroyed` is `true`, but `session.closed` is `false`.
session.destroyed
) {
return nextSession;
continue;
}
return previousSession;
});
// We only need set this once.
if (!optimalSession) {
maxConcurrentStreams = sessionMaxConcurrentStreams;
}
for (const {resolve} of listeners) {
// TODO: The session can get busy here
resolve(session);
// We're looking for the session which has biggest current pending stream count,
// in order to minimalize the amount of active sessions.
if (sessionCurrentStreamsCount > currentStreamsCount) {
optimalSession = session;
currentStreamsCount = sessionCurrentStreamsCount;
}
}
}
if (optimalSession) {
/* istanbul ignore next: safety check */
if (listeners.length !== 1) {
for (const {reject} of listeners) {
const error = new Error(
`Expected the length of listeners to be 1, got ${listeners.length}.\n` +
'Please report this to https://github.com/szmarczak/http2-wrapper/'
);
reject(error);
}
return;
}
listeners[0].resolve(optimalSession);
return;

@@ -280,2 +324,5 @@ }

// This shouldn't be executed here.
// See the comment inside _tryToCreateNewSession.
this._tryToCreateNewSession(normalizedOptions, normalizedOrigin);
return;

@@ -305,60 +352,24 @@ }

let receivedSettings = false;
let servername;
try {
const tlsSessionCache = this.tlsSessionCache.get(name);
const session = http2.connect(origin, {
createConnection: this.createConnection,
settings: this.settings,
session: tlsSessionCache ? tlsSessionCache.session : undefined,
session: this.tlsSessionCache.get(name),
...options
});
session[kCurrentStreamsCount] = 0;
session[kGracefullyClosing] = false;
// Tries to free the session.
const freeSession = () => {
// Fetch the smallest amount of free sessions of any origin we have.
const freeSessionsCount = session[kOriginSet].reduce((accumulator, origin) => {
return Math.min(accumulator, getSessions(this.freeSessions, normalizedOptions, origin).length);
}, Infinity);
// Check the limit.
if (freeSessionsCount < this.maxFreeSessions) {
addSession(this.freeSessions, normalizedOptions, session);
return true;
}
return false;
};
const isFree = () => session[kCurrentStreamsCount] < session.remoteSettings.maxConcurrentStreams;
let wasFree = true;
session.socket.once('session', tlsSession => {
// We need to cache the servername due to a bug in OpenSSL.
setImmediate(() => {
this.tlsSessionCache.set(name, {
session: tlsSession,
servername
});
});
this.tlsSessionCache.set(name, tlsSession);
});
// OpenSSL bug workaround.
// See https://github.com/nodejs/node/issues/28985
session.socket.once('secureConnect', () => {
servername = session.socket.servername;
if (servername === false && typeof tlsSessionCache !== 'undefined' && typeof tlsSessionCache.servername !== 'undefined') {
session.socket.servername = tlsSessionCache.servername;
}
});
session.once('error', error => {
// `receivedSettings` is true when the session has successfully connected.
if (!receivedSettings) {
for (const {reject} of listeners) {
reject(error);
}
// Listeners are empty when the session successfully connected.
for (const {reject} of listeners) {
reject(error);
}

@@ -372,2 +383,3 @@

// Terminates all streams owned by this session.
// TODO: Maybe the streams should have a "Session timed out" error?
session.destroy();

@@ -377,5 +389,25 @@ });

session.once('close', () => {
if (!receivedSettings) {
if (receivedSettings) {
// 1. If it wasn't free then no need to decrease because
// it has been decreased already in session.request().
// 2. `stream.once('close')` won't increment the count
// because the session is already closed.
if (wasFree) {
this._freeSessionsCount--;
}
this._sessionsCount--;
// This cannot be moved to the stream logic,
// because there may be a session that hadn't made a single request.
const where = this.sessions[normalizedOptions];
where.splice(where.indexOf(session), 1);
if (where.length === 0) {
delete this.sessions[normalizedOptions];
}
} else {
// Broken connection
const error = new Error('Session closed without receiving a SETTINGS frame');
error.code = 'HTTP2WRAPPER_NOSETTINGS';

@@ -385,10 +417,6 @@ for (const {reject} of listeners) {

}
removeFromQueue();
}
removeFromQueue();
// This cannot be moved to the stream logic,
// because there may be a session that hadn't made a single request.
removeSession(this.freeSessions, normalizedOptions, session);
// There may be another session awaiting.

@@ -400,3 +428,3 @@ this._tryToCreateNewSession(normalizedOptions, normalizedOrigin);

const processListeners = () => {
if (!(normalizedOptions in this.queue)) {
if (!(normalizedOptions in this.queue) || !isFree()) {
return;

@@ -416,6 +444,7 @@ }

if (this.queue[normalizedOptions][origin].listeners.length === 0) {
delete this.queue[normalizedOptions][origin];
const where = this.queue[normalizedOptions];
if (where[origin].listeners.length === 0) {
delete where[origin];
if (Object.keys(this.queue[normalizedOptions]).length === 0) {
if (Object.keys(where).length === 0) {
delete this.queue[normalizedOptions];

@@ -435,3 +464,3 @@ break;

// The Origin Set cannot shrink. No need to check if it suddenly became covered by another one.
session.once('origin', () => {
session.on('origin', () => {
session[kOriginSet] = session.originSet;

@@ -444,14 +473,15 @@

// Close covered sessions (if possible).
this._closeCoveredSessions(normalizedOptions, session);
processListeners();
// `session.remoteSettings.maxConcurrentStreams` might get increased
session.on('remoteSettings', () => {
this._closeCoveredSessions(normalizedOptions, session);
});
// Close covered sessions (if possible).
closeCoveredSessions(this.sessions[normalizedOptions], session);
});
session.once('remoteSettings', () => {
// Fix Node.js bug preventing the process from exiting
session.ref();
session.unref();
this._sessionsCount++;
// The Agent could have been destroyed already.

@@ -470,21 +500,27 @@ if (entry.destroyed) {

session[kOriginSet] = session.originSet;
{
const where = this.sessions;
if (normalizedOptions in where) {
const sessions = where[normalizedOptions];
sessions.splice(getSortedIndex(sessions, session, compareSessions), 0, session);
} else {
where[normalizedOptions] = [session];
}
}
this._freeSessionsCount += 1;
receivedSettings = true;
this.emit('session', session);
if (freeSession()) {
// Process listeners, we're free.
processListeners();
} else if (this.maxFreeSessions === 0) {
processListeners();
processListeners();
removeFromQueue();
// We're closing ASAP, when all possible requests have been made for this event loop tick.
setImmediate(() => {
session.close();
});
} else {
// Too late, another free session took these listeners.
// TODO: Close last recently used (or least used?) session
if (session[kCurrentStreamsCount] === 0 && this._freeSessionsCount > this.maxFreeSessions) {
session.close();
}
removeFromQueue();
// Check if we haven't managed to execute all listeners.

@@ -497,16 +533,8 @@ if (listeners.length !== 0) {

receivedSettings = true;
// `session.remoteSettings.maxConcurrentStreams` might get increased
session.on('remoteSettings', () => {
// Check if we're eligible to become a free session
if (isFree() && removeSession(this.busySessions, normalizedOptions, session)) {
// Check for free seats
if (freeSession()) {
processListeners();
} else {
// Assume it's still a busy session
addSession(this.busySessions, normalizedOptions, session);
}
}
processListeners();
// In case the Origin Set changes
closeCoveredSessions(this.sessions[normalizedOptions], session);
});

@@ -518,5 +546,9 @@ });

session.request = (headers, streamOptions) => {
if (session[kGracefullyClosing]) {
throw new Error('The session is gracefully closing. No new streams are allowed.');
}
const stream = session[kRequest](headers, streamOptions);
// The process won't exit until the session is closed.
// The process won't exit until the session is closed or all requests are gone.
session.ref();

@@ -526,31 +558,41 @@

// Check if we became busy
if (!isFree() && removeSession(this.freeSessions, normalizedOptions, session)) {
addSession(this.busySessions, normalizedOptions, session);
if (session[kCurrentStreamsCount] === session.remoteSettings.maxConcurrentStreams) {
this._freeSessionsCount--;
}
stream.once('close', () => {
wasFree = isFree();
--session[kCurrentStreamsCount];
if (isFree()) {
if (session[kCurrentStreamsCount] === 0) {
// All requests are finished, the process may exit now.
session.unref();
}
if (!session.destroyed && !session.closed) {
closeSessionIfCovered(this.sessions[normalizedOptions], session);
// Check if we are no longer busy and the session is not broken.
if (removeSession(this.busySessions, normalizedOptions, session) && !session.destroyed && !session.closed) {
// Check the sessions count of this authority and compare it to `maxSessionsCount`.
if (freeSession()) {
this._closeCoveredSessions(normalizedOptions, session);
if (isFree() && !session.closed) {
if (!wasFree) {
this._freeSessionsCount++;
wasFree = true;
}
const isEmpty = session[kCurrentStreamsCount] === 0;
if (isEmpty) {
session.unref();
}
if (
isEmpty &&
(
this._freeSessionsCount > this.maxFreeSessions ||
session[kGracefullyClosing]
)
) {
session.close();
} else {
closeCoveredSessions(this.sessions[normalizedOptions], session);
processListeners();
} else {
session.close();
}
}
}
if (!session.destroyed && !session.closed) {
closeSessionIfCovered(this.freeSessions, normalizedOptions, session);
}
});

@@ -583,3 +625,7 @@

resolve: session => {
resolve(session.request(headers, streamOptions));
try {
resolve(session.request(headers, streamOptions));
} catch (error) {
reject(error);
}
}

@@ -608,4 +654,4 @@ }]);

closeFreeSessions() {
for (const freeSessions of Object.values(this.freeSessions)) {
for (const session of freeSessions) {
for (const sessions of Object.values(this.sessions)) {
for (const session of sessions) {
if (session[kCurrentStreamsCount] === 0) {

@@ -619,4 +665,4 @@ session.close();

destroy(reason) {
for (const busySessions of Object.values(this.busySessions)) {
for (const session of busySessions) {
for (const sessions of Object.values(this.sessions)) {
for (const session of sessions) {
session.destroy(reason);

@@ -626,8 +672,2 @@ }

for (const freeSessions of Object.values(this.freeSessions)) {
for (const session of freeSessions) {
session.destroy(reason);
}
}
for (const entriesOfAuthority of Object.values(this.queue)) {

@@ -642,4 +682,15 @@ for (const entry of Object.values(entriesOfAuthority)) {

}
get freeSessions() {
return getSessions({agent: this, isFree: true});
}
get busySessions() {
return getSessions({agent: this, isFree: false});
}
}
Agent.kCurrentStreamsCount = kCurrentStreamsCount;
Agent.kGracefullyClosing = kGracefullyClosing;
module.exports = {

@@ -646,0 +697,0 @@ Agent,

@@ -171,2 +171,3 @@ 'use strict';

callback(new Error('The GET, HEAD and DELETE methods must NOT have a body'));
/* istanbul ignore next: Node.js 12 throws directly */
return;

@@ -173,0 +174,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc