797 lines
21 KiB
JavaScript
797 lines
21 KiB
JavaScript
'use strict';
|
|
// See https://github.com/facebook/jest/issues/2549
|
|
// eslint-disable-next-line node/prefer-global/url
|
|
const {URL} = require('url');
|
|
const EventEmitter = require('events');
|
|
const tls = require('tls');
|
|
const http2 = require('http2');
|
|
const QuickLRU = require('quick-lru');
|
|
const delayAsyncDestroy = require('./utils/delay-async-destroy.js');
|
|
|
|
const kCurrentStreamCount = Symbol('currentStreamCount');
|
|
const kRequest = Symbol('request');
|
|
const kOriginSet = Symbol('cachedOriginSet');
|
|
const kGracefullyClosing = Symbol('gracefullyClosing');
|
|
const kLength = Symbol('length');
|
|
|
|
const nameKeys = [
|
|
// Not an Agent option actually
|
|
'createConnection',
|
|
|
|
// `http2.connect()` options
|
|
'maxDeflateDynamicTableSize',
|
|
'maxSettings',
|
|
'maxSessionMemory',
|
|
'maxHeaderListPairs',
|
|
'maxOutstandingPings',
|
|
'maxReservedRemoteStreams',
|
|
'maxSendHeaderBlockLength',
|
|
'paddingStrategy',
|
|
'peerMaxConcurrentStreams',
|
|
'settings',
|
|
|
|
// `tls.connect()` source options
|
|
'family',
|
|
'localAddress',
|
|
'rejectUnauthorized',
|
|
|
|
// `tls.connect()` secure context options
|
|
'pskCallback',
|
|
'minDHSize',
|
|
|
|
// `tls.connect()` destination options
|
|
// - `servername` is automatically validated, skip it
|
|
// - `host` and `port` just describe the destination server,
|
|
'path',
|
|
'socket',
|
|
|
|
// `tls.createSecureContext()` options
|
|
'ca',
|
|
'cert',
|
|
'sigalgs',
|
|
'ciphers',
|
|
'clientCertEngine',
|
|
'crl',
|
|
'dhparam',
|
|
'ecdhCurve',
|
|
'honorCipherOrder',
|
|
'key',
|
|
'privateKeyEngine',
|
|
'privateKeyIdentifier',
|
|
'maxVersion',
|
|
'minVersion',
|
|
'pfx',
|
|
'secureOptions',
|
|
'secureProtocol',
|
|
'sessionIdContext',
|
|
'ticketKeys'
|
|
];
|
|
|
|
const getSortedIndex = (array, value, compare) => {
|
|
let low = 0;
|
|
let high = array.length;
|
|
|
|
while (low < high) {
|
|
const mid = (low + high) >>> 1;
|
|
|
|
if (compare(array[mid], value)) {
|
|
low = mid + 1;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
|
|
return low;
|
|
};
|
|
|
|
const compareSessions = (a, b) => a.remoteSettings.maxConcurrentStreams > b.remoteSettings.maxConcurrentStreams;
|
|
|
|
// See https://tools.ietf.org/html/rfc8336
|
|
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 (let index = 0; index < where.length; index++) {
|
|
const coveredSession = where[index];
|
|
|
|
if (
|
|
// Unfortunately `.every()` returns true for an empty array
|
|
coveredSession[kOriginSet].length > 0
|
|
|
|
// The set is a proper subset when its length is less than the other set.
|
|
&& coveredSession[kOriginSet].length < session[kOriginSet].length
|
|
|
|
// And the other set includes all elements of the subset.
|
|
&& coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
|
|
|
|
// Makes sure that the session can handle all requests from the covered session.
|
|
&& (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
|
|
) {
|
|
// This allows pending requests to finish and prevents making new requests.
|
|
gracefullyClose(coveredSession);
|
|
}
|
|
}
|
|
};
|
|
|
|
// This is basically inverted `closeCoveredSessions(...)`.
|
|
const closeSessionIfCovered = (where, coveredSession) => {
|
|
for (let index = 0; index < where.length; index++) {
|
|
const session = where[index];
|
|
|
|
if (
|
|
coveredSession[kOriginSet].length > 0
|
|
&& coveredSession[kOriginSet].length < session[kOriginSet].length
|
|
&& coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
|
|
&& (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
|
|
) {
|
|
gracefullyClose(coveredSession);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const gracefullyClose = session => {
|
|
session[kGracefullyClosing] = true;
|
|
|
|
if (session[kCurrentStreamCount] === 0) {
|
|
session.close();
|
|
}
|
|
};
|
|
|
|
class Agent extends EventEmitter {
|
|
constructor({timeout = 0, maxSessions = Number.POSITIVE_INFINITY, maxEmptySessions = 10, maxCachedTlsSessions = 100} = {}) {
|
|
super();
|
|
|
|
// SESSIONS[NORMALIZED_OPTIONS] = [];
|
|
this.sessions = {};
|
|
|
|
// The queue for creating new sessions. It looks like this:
|
|
// QUEUE[NORMALIZED_OPTIONS][NORMALIZED_ORIGIN] = ENTRY_FUNCTION
|
|
//
|
|
// It's faster when there are many origins. If there's only one, then QUEUE[`${options}:${origin}`] is faster.
|
|
// I guess object creation / deletion is causing the slowdown.
|
|
//
|
|
// The entry function has `listeners`, `completed` and `destroyed` properties.
|
|
// `listeners` is an array of objects containing `resolve` and `reject` functions.
|
|
// `completed` is a boolean. It's set to true after ENTRY_FUNCTION is executed.
|
|
// `destroyed` is a boolean. If it's set to true, the session will be destroyed if hasn't connected yet.
|
|
this.queue = {};
|
|
|
|
// Each session will use this timeout value.
|
|
this.timeout = timeout;
|
|
|
|
// Max sessions in total
|
|
this.maxSessions = maxSessions;
|
|
|
|
// Max empty sessions in total
|
|
this.maxEmptySessions = maxEmptySessions;
|
|
|
|
this._emptySessionCount = 0;
|
|
this._sessionCount = 0;
|
|
|
|
// We don't support push streams by default.
|
|
this.settings = {
|
|
enablePush: false,
|
|
initialWindowSize: 1024 * 1024 * 32 // 32MB, see https://github.com/nodejs/node/issues/38426
|
|
};
|
|
|
|
// Reusing TLS sessions increases performance.
|
|
this.tlsSessionCache = new QuickLRU({maxSize: maxCachedTlsSessions});
|
|
}
|
|
|
|
get protocol() {
|
|
return 'https:';
|
|
}
|
|
|
|
normalizeOptions(options) {
|
|
let normalized = '';
|
|
|
|
for (let index = 0; index < nameKeys.length; index++) {
|
|
const key = nameKeys[index];
|
|
|
|
normalized += ':';
|
|
|
|
if (options && options[key] !== undefined) {
|
|
normalized += options[key];
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
_processQueue() {
|
|
if (this._sessionCount >= this.maxSessions) {
|
|
this.closeEmptySessions(this.maxSessions - this._sessionCount + 1);
|
|
return;
|
|
}
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const normalizedOptions in this.queue) {
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const normalizedOrigin in this.queue[normalizedOptions]) {
|
|
const item = this.queue[normalizedOptions][normalizedOrigin];
|
|
|
|
// The entry function can be run only once.
|
|
if (!item.completed) {
|
|
item.completed = true;
|
|
|
|
item();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_isBetterSession(thisStreamCount, thatStreamCount) {
|
|
return thisStreamCount > thatStreamCount;
|
|
}
|
|
|
|
_accept(session, listeners, normalizedOrigin, options) {
|
|
let index = 0;
|
|
|
|
while (index < listeners.length && session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams) {
|
|
// We assume `resolve(...)` calls `request(...)` *directly*,
|
|
// otherwise the session will get overloaded.
|
|
listeners[index].resolve(session);
|
|
|
|
index++;
|
|
}
|
|
|
|
listeners.splice(0, index);
|
|
|
|
if (listeners.length > 0) {
|
|
this.getSession(normalizedOrigin, options, listeners);
|
|
listeners.length = 0;
|
|
}
|
|
}
|
|
|
|
getSession(origin, options, listeners) {
|
|
return new Promise((resolve, reject) => {
|
|
if (Array.isArray(listeners) && listeners.length > 0) {
|
|
listeners = [...listeners];
|
|
|
|
// Resolve the current promise ASAP, we're just moving the listeners.
|
|
// They will be executed at a different time.
|
|
resolve();
|
|
} else {
|
|
listeners = [{resolve, reject}];
|
|
}
|
|
|
|
try {
|
|
// Parse origin
|
|
if (typeof origin === 'string') {
|
|
origin = new URL(origin);
|
|
} else if (!(origin instanceof URL)) {
|
|
throw new TypeError('The `origin` argument needs to be a string or an URL object');
|
|
}
|
|
|
|
if (options) {
|
|
// Validate servername
|
|
const {servername} = options;
|
|
const {hostname} = origin;
|
|
if (servername && hostname !== servername) {
|
|
throw new Error(`Origin ${hostname} differs from servername ${servername}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
for (let index = 0; index < listeners.length; index++) {
|
|
listeners[index].reject(error);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const normalizedOptions = this.normalizeOptions(options);
|
|
const normalizedOrigin = origin.origin;
|
|
|
|
if (normalizedOptions in this.sessions) {
|
|
const sessions = this.sessions[normalizedOptions];
|
|
|
|
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.
|
|
//
|
|
// |------------| |------------| |------------| |------------|
|
|
// | Session: A | | Session: B | | Session: C | | Session: D |
|
|
// | Pending: 5 |-| Pending: 8 |-| Pending: 9 |-| Pending: 4 |
|
|
// | Max: 10 | | Max: 10 | | Max: 9 | | Max: 5 |
|
|
// |------------| |------------| |------------| |------------|
|
|
// ^
|
|
// |
|
|
// pick this one --
|
|
//
|
|
for (let index = 0; index < sessions.length; index++) {
|
|
const session = sessions[index];
|
|
|
|
const sessionMaxConcurrentStreams = session.remoteSettings.maxConcurrentStreams;
|
|
|
|
if (sessionMaxConcurrentStreams < maxConcurrentStreams) {
|
|
break;
|
|
}
|
|
|
|
if (!session[kOriginSet].includes(normalizedOrigin)) {
|
|
continue;
|
|
}
|
|
|
|
const sessionCurrentStreamsCount = session[kCurrentStreamCount];
|
|
|
|
if (
|
|
sessionCurrentStreamsCount >= sessionMaxConcurrentStreams
|
|
|| session[kGracefullyClosing]
|
|
// Unfortunately the `close` event isn't called immediately,
|
|
// so `session.destroyed` is `true`, but `session.closed` is `false`.
|
|
|| session.destroyed
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// We only need set this once.
|
|
if (!optimalSession) {
|
|
maxConcurrentStreams = sessionMaxConcurrentStreams;
|
|
}
|
|
|
|
// Either get the session which has biggest current stream count or the lowest.
|
|
if (this._isBetterSession(sessionCurrentStreamsCount, currentStreamsCount)) {
|
|
optimalSession = session;
|
|
currentStreamsCount = sessionCurrentStreamsCount;
|
|
}
|
|
}
|
|
|
|
if (optimalSession) {
|
|
this._accept(optimalSession, listeners, normalizedOrigin, options);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (normalizedOptions in this.queue) {
|
|
if (normalizedOrigin in this.queue[normalizedOptions]) {
|
|
// There's already an item in the queue, just attach ourselves to it.
|
|
this.queue[normalizedOptions][normalizedOrigin].listeners.push(...listeners);
|
|
return;
|
|
}
|
|
} else {
|
|
this.queue[normalizedOptions] = {
|
|
[kLength]: 0
|
|
};
|
|
}
|
|
|
|
// The entry must be removed from the queue IMMEDIATELY when:
|
|
// 1. the session connects successfully,
|
|
// 2. an error occurs.
|
|
const removeFromQueue = () => {
|
|
// Our entry can be replaced. We cannot remove the new one.
|
|
if (normalizedOptions in this.queue && this.queue[normalizedOptions][normalizedOrigin] === entry) {
|
|
delete this.queue[normalizedOptions][normalizedOrigin];
|
|
|
|
if (--this.queue[normalizedOptions][kLength] === 0) {
|
|
delete this.queue[normalizedOptions];
|
|
}
|
|
}
|
|
};
|
|
|
|
// The main logic is here
|
|
const entry = async () => {
|
|
this._sessionCount++;
|
|
|
|
const name = `${normalizedOrigin}:${normalizedOptions}`;
|
|
let receivedSettings = false;
|
|
let socket;
|
|
|
|
try {
|
|
const computedOptions = {...options};
|
|
|
|
if (computedOptions.settings === undefined) {
|
|
computedOptions.settings = this.settings;
|
|
}
|
|
|
|
if (computedOptions.session === undefined) {
|
|
computedOptions.session = this.tlsSessionCache.get(name);
|
|
}
|
|
|
|
const createConnection = computedOptions.createConnection || this.createConnection;
|
|
|
|
// A hacky workaround to enable async `createConnection`
|
|
socket = await createConnection.call(this, origin, computedOptions);
|
|
computedOptions.createConnection = () => socket;
|
|
|
|
const session = http2.connect(origin, computedOptions);
|
|
session[kCurrentStreamCount] = 0;
|
|
session[kGracefullyClosing] = false;
|
|
|
|
// Node.js return https://false:443 instead of https://1.1.1.1:443
|
|
const getOriginSet = () => {
|
|
const {socket} = session;
|
|
|
|
let originSet;
|
|
if (socket.servername === false) {
|
|
socket.servername = socket.remoteAddress;
|
|
originSet = session.originSet;
|
|
socket.servername = false;
|
|
} else {
|
|
originSet = session.originSet;
|
|
}
|
|
|
|
return originSet;
|
|
};
|
|
|
|
const isFree = () => session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams;
|
|
|
|
session.socket.once('session', tlsSession => {
|
|
this.tlsSessionCache.set(name, tlsSession);
|
|
});
|
|
|
|
session.once('error', error => {
|
|
// Listeners are empty when the session successfully connected.
|
|
for (let index = 0; index < listeners.length; index++) {
|
|
listeners[index].reject(error);
|
|
}
|
|
|
|
// The connection got broken, purge the cache.
|
|
this.tlsSessionCache.delete(name);
|
|
});
|
|
|
|
session.setTimeout(this.timeout, () => {
|
|
// Terminates all streams owned by this session.
|
|
session.destroy();
|
|
});
|
|
|
|
session.once('close', () => {
|
|
this._sessionCount--;
|
|
|
|
if (receivedSettings) {
|
|
// Assumes session `close` is emitted after request `close`
|
|
this._emptySessionCount--;
|
|
|
|
// 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];
|
|
|
|
if (where.length === 1) {
|
|
delete this.sessions[normalizedOptions];
|
|
} else {
|
|
where.splice(where.indexOf(session), 1);
|
|
}
|
|
} else {
|
|
// Broken connection
|
|
removeFromQueue();
|
|
|
|
const error = new Error('Session closed without receiving a SETTINGS frame');
|
|
error.code = 'HTTP2WRAPPER_NOSETTINGS';
|
|
|
|
for (let index = 0; index < listeners.length; index++) {
|
|
listeners[index].reject(error);
|
|
}
|
|
}
|
|
|
|
// There may be another session awaiting.
|
|
this._processQueue();
|
|
});
|
|
|
|
// Iterates over the queue and processes listeners.
|
|
const processListeners = () => {
|
|
const queue = this.queue[normalizedOptions];
|
|
if (!queue) {
|
|
return;
|
|
}
|
|
|
|
const originSet = session[kOriginSet];
|
|
|
|
for (let index = 0; index < originSet.length; index++) {
|
|
const origin = originSet[index];
|
|
|
|
if (origin in queue) {
|
|
const {listeners, completed} = queue[origin];
|
|
|
|
let index = 0;
|
|
|
|
// Prevents session overloading.
|
|
while (index < listeners.length && isFree()) {
|
|
// We assume `resolve(...)` calls `request(...)` *directly*,
|
|
// otherwise the session will get overloaded.
|
|
listeners[index].resolve(session);
|
|
|
|
index++;
|
|
}
|
|
|
|
queue[origin].listeners.splice(0, index);
|
|
|
|
if (queue[origin].listeners.length === 0 && !completed) {
|
|
delete queue[origin];
|
|
|
|
if (--queue[kLength] === 0) {
|
|
delete this.queue[normalizedOptions];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We're no longer free, no point in continuing.
|
|
if (!isFree()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// The Origin Set cannot shrink. No need to check if it suddenly became covered by another one.
|
|
session.on('origin', () => {
|
|
session[kOriginSet] = getOriginSet() || [];
|
|
session[kGracefullyClosing] = false;
|
|
closeSessionIfCovered(this.sessions[normalizedOptions], session);
|
|
|
|
if (session[kGracefullyClosing] || !isFree()) {
|
|
return;
|
|
}
|
|
|
|
processListeners();
|
|
|
|
if (!isFree()) {
|
|
return;
|
|
}
|
|
|
|
// Close covered sessions (if possible).
|
|
closeCoveredSessions(this.sessions[normalizedOptions], session);
|
|
});
|
|
|
|
session.once('remoteSettings', () => {
|
|
// The Agent could have been destroyed already.
|
|
if (entry.destroyed) {
|
|
const error = new Error('Agent has been destroyed');
|
|
|
|
for (let index = 0; index < listeners.length; index++) {
|
|
listeners[index].reject(error);
|
|
}
|
|
|
|
session.destroy();
|
|
return;
|
|
}
|
|
|
|
// See https://github.com/nodejs/node/issues/38426
|
|
if (session.setLocalWindowSize) {
|
|
session.setLocalWindowSize(1024 * 1024 * 4); // 4 MB
|
|
}
|
|
|
|
session[kOriginSet] = getOriginSet() || [];
|
|
|
|
if (session.socket.encrypted) {
|
|
const mainOrigin = session[kOriginSet][0];
|
|
if (mainOrigin !== normalizedOrigin) {
|
|
const error = new Error(`Requested origin ${normalizedOrigin} does not match server ${mainOrigin}`);
|
|
|
|
for (let index = 0; index < listeners.length; index++) {
|
|
listeners[index].reject(error);
|
|
}
|
|
|
|
session.destroy();
|
|
return;
|
|
}
|
|
}
|
|
|
|
removeFromQueue();
|
|
|
|
{
|
|
const where = this.sessions;
|
|
|
|
if (normalizedOptions in where) {
|
|
const sessions = where[normalizedOptions];
|
|
sessions.splice(getSortedIndex(sessions, session, compareSessions), 0, session);
|
|
} else {
|
|
where[normalizedOptions] = [session];
|
|
}
|
|
}
|
|
|
|
receivedSettings = true;
|
|
this._emptySessionCount++;
|
|
|
|
this.emit('session', session);
|
|
this._accept(session, listeners, normalizedOrigin, options);
|
|
|
|
if (session[kCurrentStreamCount] === 0 && this._emptySessionCount > this.maxEmptySessions) {
|
|
this.closeEmptySessions(this._emptySessionCount - this.maxEmptySessions);
|
|
}
|
|
|
|
// `session.remoteSettings.maxConcurrentStreams` might get increased
|
|
session.on('remoteSettings', () => {
|
|
if (!isFree()) {
|
|
return;
|
|
}
|
|
|
|
processListeners();
|
|
|
|
if (!isFree()) {
|
|
return;
|
|
}
|
|
|
|
// In case the Origin Set changes
|
|
closeCoveredSessions(this.sessions[normalizedOptions], session);
|
|
});
|
|
});
|
|
|
|
// Shim `session.request()` in order to catch all streams
|
|
session[kRequest] = session.request;
|
|
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 or all requests are gone.
|
|
session.ref();
|
|
|
|
if (session[kCurrentStreamCount]++ === 0) {
|
|
this._emptySessionCount--;
|
|
}
|
|
|
|
stream.once('close', () => {
|
|
if (--session[kCurrentStreamCount] === 0) {
|
|
this._emptySessionCount++;
|
|
session.unref();
|
|
|
|
if (this._emptySessionCount > this.maxEmptySessions || session[kGracefullyClosing]) {
|
|
session.close();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (session.destroyed || session.closed) {
|
|
return;
|
|
}
|
|
|
|
if (isFree() && !closeSessionIfCovered(this.sessions[normalizedOptions], session)) {
|
|
closeCoveredSessions(this.sessions[normalizedOptions], session);
|
|
processListeners();
|
|
|
|
if (session[kCurrentStreamCount] === 0) {
|
|
this._processQueue();
|
|
}
|
|
}
|
|
});
|
|
|
|
return stream;
|
|
};
|
|
} catch (error) {
|
|
removeFromQueue();
|
|
this._sessionCount--;
|
|
|
|
for (let index = 0; index < listeners.length; index++) {
|
|
listeners[index].reject(error);
|
|
}
|
|
}
|
|
};
|
|
|
|
entry.listeners = listeners;
|
|
entry.completed = false;
|
|
entry.destroyed = false;
|
|
|
|
this.queue[normalizedOptions][normalizedOrigin] = entry;
|
|
this.queue[normalizedOptions][kLength]++;
|
|
this._processQueue();
|
|
});
|
|
}
|
|
|
|
request(origin, options, headers, streamOptions) {
|
|
return new Promise((resolve, reject) => {
|
|
this.getSession(origin, options, [{
|
|
reject,
|
|
resolve: session => {
|
|
try {
|
|
const stream = session.request(headers, streamOptions);
|
|
|
|
// Do not throw before `request(...)` has been awaited
|
|
delayAsyncDestroy(stream);
|
|
|
|
resolve(stream);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
}
|
|
}]);
|
|
});
|
|
}
|
|
|
|
async createConnection(origin, options) {
|
|
return Agent.connect(origin, options);
|
|
}
|
|
|
|
static connect(origin, options) {
|
|
options.ALPNProtocols = ['h2'];
|
|
|
|
const port = origin.port || 443;
|
|
const host = origin.hostname;
|
|
|
|
if (typeof options.servername === 'undefined') {
|
|
options.servername = host;
|
|
}
|
|
|
|
const socket = tls.connect(port, host, options);
|
|
|
|
if (options.socket) {
|
|
socket._peername = {
|
|
family: undefined,
|
|
address: undefined,
|
|
port
|
|
};
|
|
}
|
|
|
|
return socket;
|
|
}
|
|
|
|
closeEmptySessions(maxCount = Number.POSITIVE_INFINITY) {
|
|
let closedCount = 0;
|
|
|
|
const {sessions} = this;
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const key in sessions) {
|
|
const thisSessions = sessions[key];
|
|
|
|
for (let index = 0; index < thisSessions.length; index++) {
|
|
const session = thisSessions[index];
|
|
|
|
if (session[kCurrentStreamCount] === 0) {
|
|
closedCount++;
|
|
session.close();
|
|
|
|
if (closedCount >= maxCount) {
|
|
return closedCount;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return closedCount;
|
|
}
|
|
|
|
destroy(reason) {
|
|
const {sessions, queue} = this;
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const key in sessions) {
|
|
const thisSessions = sessions[key];
|
|
|
|
for (let index = 0; index < thisSessions.length; index++) {
|
|
thisSessions[index].destroy(reason);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const normalizedOptions in queue) {
|
|
const entries = queue[normalizedOptions];
|
|
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const normalizedOrigin in entries) {
|
|
entries[normalizedOrigin].destroyed = true;
|
|
}
|
|
}
|
|
|
|
// New requests should NOT attach to destroyed sessions
|
|
this.queue = {};
|
|
this.tlsSessionCache.clear();
|
|
}
|
|
|
|
get emptySessionCount() {
|
|
return this._emptySessionCount;
|
|
}
|
|
|
|
get pendingSessionCount() {
|
|
return this._sessionCount - this._emptySessionCount;
|
|
}
|
|
|
|
get sessionCount() {
|
|
return this._sessionCount;
|
|
}
|
|
}
|
|
|
|
Agent.kCurrentStreamCount = kCurrentStreamCount;
|
|
Agent.kGracefullyClosing = kGracefullyClosing;
|
|
|
|
module.exports = {
|
|
Agent,
|
|
globalAgent: new Agent()
|
|
};
|