agent.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  1. 'use strict';
  2. // See https://github.com/facebook/jest/issues/2549
  3. // eslint-disable-next-line node/prefer-global/url
  4. const {URL} = require('url');
  5. const EventEmitter = require('events');
  6. const tls = require('tls');
  7. const http2 = require('http2');
  8. const QuickLRU = require('quick-lru');
  9. const delayAsyncDestroy = require('./utils/delay-async-destroy.js');
  10. const kCurrentStreamCount = Symbol('currentStreamCount');
  11. const kRequest = Symbol('request');
  12. const kOriginSet = Symbol('cachedOriginSet');
  13. const kGracefullyClosing = Symbol('gracefullyClosing');
  14. const kLength = Symbol('length');
  15. const nameKeys = [
  16. // Not an Agent option actually
  17. 'createConnection',
  18. // `http2.connect()` options
  19. 'maxDeflateDynamicTableSize',
  20. 'maxSettings',
  21. 'maxSessionMemory',
  22. 'maxHeaderListPairs',
  23. 'maxOutstandingPings',
  24. 'maxReservedRemoteStreams',
  25. 'maxSendHeaderBlockLength',
  26. 'paddingStrategy',
  27. 'peerMaxConcurrentStreams',
  28. 'settings',
  29. // `tls.connect()` source options
  30. 'family',
  31. 'localAddress',
  32. 'rejectUnauthorized',
  33. // `tls.connect()` secure context options
  34. 'pskCallback',
  35. 'minDHSize',
  36. // `tls.connect()` destination options
  37. // - `servername` is automatically validated, skip it
  38. // - `host` and `port` just describe the destination server,
  39. 'path',
  40. 'socket',
  41. // `tls.createSecureContext()` options
  42. 'ca',
  43. 'cert',
  44. 'sigalgs',
  45. 'ciphers',
  46. 'clientCertEngine',
  47. 'crl',
  48. 'dhparam',
  49. 'ecdhCurve',
  50. 'honorCipherOrder',
  51. 'key',
  52. 'privateKeyEngine',
  53. 'privateKeyIdentifier',
  54. 'maxVersion',
  55. 'minVersion',
  56. 'pfx',
  57. 'secureOptions',
  58. 'secureProtocol',
  59. 'sessionIdContext',
  60. 'ticketKeys'
  61. ];
  62. const getSortedIndex = (array, value, compare) => {
  63. let low = 0;
  64. let high = array.length;
  65. while (low < high) {
  66. const mid = (low + high) >>> 1;
  67. if (compare(array[mid], value)) {
  68. low = mid + 1;
  69. } else {
  70. high = mid;
  71. }
  72. }
  73. return low;
  74. };
  75. const compareSessions = (a, b) => a.remoteSettings.maxConcurrentStreams > b.remoteSettings.maxConcurrentStreams;
  76. // See https://tools.ietf.org/html/rfc8336
  77. const closeCoveredSessions = (where, session) => {
  78. // Clients SHOULD NOT emit new requests on any connection whose Origin
  79. // Set is a proper subset of another connection's Origin Set, and they
  80. // SHOULD close it once all outstanding requests are satisfied.
  81. for (let index = 0; index < where.length; index++) {
  82. const coveredSession = where[index];
  83. if (
  84. // Unfortunately `.every()` returns true for an empty array
  85. coveredSession[kOriginSet].length > 0
  86. // The set is a proper subset when its length is less than the other set.
  87. && coveredSession[kOriginSet].length < session[kOriginSet].length
  88. // And the other set includes all elements of the subset.
  89. && coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
  90. // Makes sure that the session can handle all requests from the covered session.
  91. && (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
  92. ) {
  93. // This allows pending requests to finish and prevents making new requests.
  94. gracefullyClose(coveredSession);
  95. }
  96. }
  97. };
  98. // This is basically inverted `closeCoveredSessions(...)`.
  99. const closeSessionIfCovered = (where, coveredSession) => {
  100. for (let index = 0; index < where.length; index++) {
  101. const session = where[index];
  102. if (
  103. coveredSession[kOriginSet].length > 0
  104. && coveredSession[kOriginSet].length < session[kOriginSet].length
  105. && coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
  106. && (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
  107. ) {
  108. gracefullyClose(coveredSession);
  109. return true;
  110. }
  111. }
  112. return false;
  113. };
  114. const gracefullyClose = session => {
  115. session[kGracefullyClosing] = true;
  116. if (session[kCurrentStreamCount] === 0) {
  117. session.close();
  118. }
  119. };
  120. class Agent extends EventEmitter {
  121. constructor({timeout = 0, maxSessions = Number.POSITIVE_INFINITY, maxEmptySessions = 10, maxCachedTlsSessions = 100} = {}) {
  122. super();
  123. // SESSIONS[NORMALIZED_OPTIONS] = [];
  124. this.sessions = {};
  125. // The queue for creating new sessions. It looks like this:
  126. // QUEUE[NORMALIZED_OPTIONS][NORMALIZED_ORIGIN] = ENTRY_FUNCTION
  127. //
  128. // It's faster when there are many origins. If there's only one, then QUEUE[`${options}:${origin}`] is faster.
  129. // I guess object creation / deletion is causing the slowdown.
  130. //
  131. // The entry function has `listeners`, `completed` and `destroyed` properties.
  132. // `listeners` is an array of objects containing `resolve` and `reject` functions.
  133. // `completed` is a boolean. It's set to true after ENTRY_FUNCTION is executed.
  134. // `destroyed` is a boolean. If it's set to true, the session will be destroyed if hasn't connected yet.
  135. this.queue = {};
  136. // Each session will use this timeout value.
  137. this.timeout = timeout;
  138. // Max sessions in total
  139. this.maxSessions = maxSessions;
  140. // Max empty sessions in total
  141. this.maxEmptySessions = maxEmptySessions;
  142. this._emptySessionCount = 0;
  143. this._sessionCount = 0;
  144. // We don't support push streams by default.
  145. this.settings = {
  146. enablePush: false,
  147. initialWindowSize: 1024 * 1024 * 32 // 32MB, see https://github.com/nodejs/node/issues/38426
  148. };
  149. // Reusing TLS sessions increases performance.
  150. this.tlsSessionCache = new QuickLRU({maxSize: maxCachedTlsSessions});
  151. }
  152. get protocol() {
  153. return 'https:';
  154. }
  155. normalizeOptions(options) {
  156. let normalized = '';
  157. for (let index = 0; index < nameKeys.length; index++) {
  158. const key = nameKeys[index];
  159. normalized += ':';
  160. if (options && options[key] !== undefined) {
  161. normalized += options[key];
  162. }
  163. }
  164. return normalized;
  165. }
  166. _processQueue() {
  167. if (this._sessionCount >= this.maxSessions) {
  168. this.closeEmptySessions(this.maxSessions - this._sessionCount + 1);
  169. return;
  170. }
  171. // eslint-disable-next-line guard-for-in
  172. for (const normalizedOptions in this.queue) {
  173. // eslint-disable-next-line guard-for-in
  174. for (const normalizedOrigin in this.queue[normalizedOptions]) {
  175. const item = this.queue[normalizedOptions][normalizedOrigin];
  176. // The entry function can be run only once.
  177. if (!item.completed) {
  178. item.completed = true;
  179. item();
  180. }
  181. }
  182. }
  183. }
  184. _isBetterSession(thisStreamCount, thatStreamCount) {
  185. return thisStreamCount > thatStreamCount;
  186. }
  187. _accept(session, listeners, normalizedOrigin, options) {
  188. let index = 0;
  189. while (index < listeners.length && session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams) {
  190. // We assume `resolve(...)` calls `request(...)` *directly*,
  191. // otherwise the session will get overloaded.
  192. listeners[index].resolve(session);
  193. index++;
  194. }
  195. listeners.splice(0, index);
  196. if (listeners.length > 0) {
  197. this.getSession(normalizedOrigin, options, listeners);
  198. listeners.length = 0;
  199. }
  200. }
  201. getSession(origin, options, listeners) {
  202. return new Promise((resolve, reject) => {
  203. if (Array.isArray(listeners) && listeners.length > 0) {
  204. listeners = [...listeners];
  205. // Resolve the current promise ASAP, we're just moving the listeners.
  206. // They will be executed at a different time.
  207. resolve();
  208. } else {
  209. listeners = [{resolve, reject}];
  210. }
  211. try {
  212. // Parse origin
  213. if (typeof origin === 'string') {
  214. origin = new URL(origin);
  215. } else if (!(origin instanceof URL)) {
  216. throw new TypeError('The `origin` argument needs to be a string or an URL object');
  217. }
  218. if (options) {
  219. // Validate servername
  220. const {servername} = options;
  221. const {hostname} = origin;
  222. if (servername && hostname !== servername) {
  223. throw new Error(`Origin ${hostname} differs from servername ${servername}`);
  224. }
  225. }
  226. } catch (error) {
  227. for (let index = 0; index < listeners.length; index++) {
  228. listeners[index].reject(error);
  229. }
  230. return;
  231. }
  232. const normalizedOptions = this.normalizeOptions(options);
  233. const normalizedOrigin = origin.origin;
  234. if (normalizedOptions in this.sessions) {
  235. const sessions = this.sessions[normalizedOptions];
  236. let maxConcurrentStreams = -1;
  237. let currentStreamsCount = -1;
  238. let optimalSession;
  239. // We could just do this.sessions[normalizedOptions].find(...) but that isn't optimal.
  240. // Additionally, we are looking for session which has biggest current pending streams count.
  241. //
  242. // |------------| |------------| |------------| |------------|
  243. // | Session: A | | Session: B | | Session: C | | Session: D |
  244. // | Pending: 5 |-| Pending: 8 |-| Pending: 9 |-| Pending: 4 |
  245. // | Max: 10 | | Max: 10 | | Max: 9 | | Max: 5 |
  246. // |------------| |------------| |------------| |------------|
  247. // ^
  248. // |
  249. // pick this one --
  250. //
  251. for (let index = 0; index < sessions.length; index++) {
  252. const session = sessions[index];
  253. const sessionMaxConcurrentStreams = session.remoteSettings.maxConcurrentStreams;
  254. if (sessionMaxConcurrentStreams < maxConcurrentStreams) {
  255. break;
  256. }
  257. if (!session[kOriginSet].includes(normalizedOrigin)) {
  258. continue;
  259. }
  260. const sessionCurrentStreamsCount = session[kCurrentStreamCount];
  261. if (
  262. sessionCurrentStreamsCount >= sessionMaxConcurrentStreams
  263. || session[kGracefullyClosing]
  264. // Unfortunately the `close` event isn't called immediately,
  265. // so `session.destroyed` is `true`, but `session.closed` is `false`.
  266. || session.destroyed
  267. ) {
  268. continue;
  269. }
  270. // We only need set this once.
  271. if (!optimalSession) {
  272. maxConcurrentStreams = sessionMaxConcurrentStreams;
  273. }
  274. // Either get the session which has biggest current stream count or the lowest.
  275. if (this._isBetterSession(sessionCurrentStreamsCount, currentStreamsCount)) {
  276. optimalSession = session;
  277. currentStreamsCount = sessionCurrentStreamsCount;
  278. }
  279. }
  280. if (optimalSession) {
  281. this._accept(optimalSession, listeners, normalizedOrigin, options);
  282. return;
  283. }
  284. }
  285. if (normalizedOptions in this.queue) {
  286. if (normalizedOrigin in this.queue[normalizedOptions]) {
  287. // There's already an item in the queue, just attach ourselves to it.
  288. this.queue[normalizedOptions][normalizedOrigin].listeners.push(...listeners);
  289. return;
  290. }
  291. } else {
  292. this.queue[normalizedOptions] = {
  293. [kLength]: 0
  294. };
  295. }
  296. // The entry must be removed from the queue IMMEDIATELY when:
  297. // 1. the session connects successfully,
  298. // 2. an error occurs.
  299. const removeFromQueue = () => {
  300. // Our entry can be replaced. We cannot remove the new one.
  301. if (normalizedOptions in this.queue && this.queue[normalizedOptions][normalizedOrigin] === entry) {
  302. delete this.queue[normalizedOptions][normalizedOrigin];
  303. if (--this.queue[normalizedOptions][kLength] === 0) {
  304. delete this.queue[normalizedOptions];
  305. }
  306. }
  307. };
  308. // The main logic is here
  309. const entry = async () => {
  310. this._sessionCount++;
  311. const name = `${normalizedOrigin}:${normalizedOptions}`;
  312. let receivedSettings = false;
  313. let socket;
  314. try {
  315. const computedOptions = {...options};
  316. if (computedOptions.settings === undefined) {
  317. computedOptions.settings = this.settings;
  318. }
  319. if (computedOptions.session === undefined) {
  320. computedOptions.session = this.tlsSessionCache.get(name);
  321. }
  322. const createConnection = computedOptions.createConnection || this.createConnection;
  323. // A hacky workaround to enable async `createConnection`
  324. socket = await createConnection.call(this, origin, computedOptions);
  325. computedOptions.createConnection = () => socket;
  326. const session = http2.connect(origin, computedOptions);
  327. session[kCurrentStreamCount] = 0;
  328. session[kGracefullyClosing] = false;
  329. // Node.js return https://false:443 instead of https://1.1.1.1:443
  330. const getOriginSet = () => {
  331. const {socket} = session;
  332. let originSet;
  333. if (socket.servername === false) {
  334. socket.servername = socket.remoteAddress;
  335. originSet = session.originSet;
  336. socket.servername = false;
  337. } else {
  338. originSet = session.originSet;
  339. }
  340. return originSet;
  341. };
  342. const isFree = () => session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams;
  343. session.socket.once('session', tlsSession => {
  344. this.tlsSessionCache.set(name, tlsSession);
  345. });
  346. session.once('error', error => {
  347. // Listeners are empty when the session successfully connected.
  348. for (let index = 0; index < listeners.length; index++) {
  349. listeners[index].reject(error);
  350. }
  351. // The connection got broken, purge the cache.
  352. this.tlsSessionCache.delete(name);
  353. });
  354. session.setTimeout(this.timeout, () => {
  355. // Terminates all streams owned by this session.
  356. session.destroy();
  357. });
  358. session.once('close', () => {
  359. this._sessionCount--;
  360. if (receivedSettings) {
  361. // Assumes session `close` is emitted after request `close`
  362. this._emptySessionCount--;
  363. // This cannot be moved to the stream logic,
  364. // because there may be a session that hadn't made a single request.
  365. const where = this.sessions[normalizedOptions];
  366. if (where.length === 1) {
  367. delete this.sessions[normalizedOptions];
  368. } else {
  369. where.splice(where.indexOf(session), 1);
  370. }
  371. } else {
  372. // Broken connection
  373. removeFromQueue();
  374. const error = new Error('Session closed without receiving a SETTINGS frame');
  375. error.code = 'HTTP2WRAPPER_NOSETTINGS';
  376. for (let index = 0; index < listeners.length; index++) {
  377. listeners[index].reject(error);
  378. }
  379. }
  380. // There may be another session awaiting.
  381. this._processQueue();
  382. });
  383. // Iterates over the queue and processes listeners.
  384. const processListeners = () => {
  385. const queue = this.queue[normalizedOptions];
  386. if (!queue) {
  387. return;
  388. }
  389. const originSet = session[kOriginSet];
  390. for (let index = 0; index < originSet.length; index++) {
  391. const origin = originSet[index];
  392. if (origin in queue) {
  393. const {listeners, completed} = queue[origin];
  394. let index = 0;
  395. // Prevents session overloading.
  396. while (index < listeners.length && isFree()) {
  397. // We assume `resolve(...)` calls `request(...)` *directly*,
  398. // otherwise the session will get overloaded.
  399. listeners[index].resolve(session);
  400. index++;
  401. }
  402. queue[origin].listeners.splice(0, index);
  403. if (queue[origin].listeners.length === 0 && !completed) {
  404. delete queue[origin];
  405. if (--queue[kLength] === 0) {
  406. delete this.queue[normalizedOptions];
  407. break;
  408. }
  409. }
  410. // We're no longer free, no point in continuing.
  411. if (!isFree()) {
  412. break;
  413. }
  414. }
  415. }
  416. };
  417. // The Origin Set cannot shrink. No need to check if it suddenly became covered by another one.
  418. session.on('origin', () => {
  419. session[kOriginSet] = getOriginSet() || [];
  420. session[kGracefullyClosing] = false;
  421. closeSessionIfCovered(this.sessions[normalizedOptions], session);
  422. if (session[kGracefullyClosing] || !isFree()) {
  423. return;
  424. }
  425. processListeners();
  426. if (!isFree()) {
  427. return;
  428. }
  429. // Close covered sessions (if possible).
  430. closeCoveredSessions(this.sessions[normalizedOptions], session);
  431. });
  432. session.once('remoteSettings', () => {
  433. // The Agent could have been destroyed already.
  434. if (entry.destroyed) {
  435. const error = new Error('Agent has been destroyed');
  436. for (let index = 0; index < listeners.length; index++) {
  437. listeners[index].reject(error);
  438. }
  439. session.destroy();
  440. return;
  441. }
  442. // See https://github.com/nodejs/node/issues/38426
  443. if (session.setLocalWindowSize) {
  444. session.setLocalWindowSize(1024 * 1024 * 4); // 4 MB
  445. }
  446. session[kOriginSet] = getOriginSet() || [];
  447. if (session.socket.encrypted) {
  448. const mainOrigin = session[kOriginSet][0];
  449. if (mainOrigin !== normalizedOrigin) {
  450. const error = new Error(`Requested origin ${normalizedOrigin} does not match server ${mainOrigin}`);
  451. for (let index = 0; index < listeners.length; index++) {
  452. listeners[index].reject(error);
  453. }
  454. session.destroy();
  455. return;
  456. }
  457. }
  458. removeFromQueue();
  459. {
  460. const where = this.sessions;
  461. if (normalizedOptions in where) {
  462. const sessions = where[normalizedOptions];
  463. sessions.splice(getSortedIndex(sessions, session, compareSessions), 0, session);
  464. } else {
  465. where[normalizedOptions] = [session];
  466. }
  467. }
  468. receivedSettings = true;
  469. this._emptySessionCount++;
  470. this.emit('session', session);
  471. this._accept(session, listeners, normalizedOrigin, options);
  472. if (session[kCurrentStreamCount] === 0 && this._emptySessionCount > this.maxEmptySessions) {
  473. this.closeEmptySessions(this._emptySessionCount - this.maxEmptySessions);
  474. }
  475. // `session.remoteSettings.maxConcurrentStreams` might get increased
  476. session.on('remoteSettings', () => {
  477. if (!isFree()) {
  478. return;
  479. }
  480. processListeners();
  481. if (!isFree()) {
  482. return;
  483. }
  484. // In case the Origin Set changes
  485. closeCoveredSessions(this.sessions[normalizedOptions], session);
  486. });
  487. });
  488. // Shim `session.request()` in order to catch all streams
  489. session[kRequest] = session.request;
  490. session.request = (headers, streamOptions) => {
  491. if (session[kGracefullyClosing]) {
  492. throw new Error('The session is gracefully closing. No new streams are allowed.');
  493. }
  494. const stream = session[kRequest](headers, streamOptions);
  495. // The process won't exit until the session is closed or all requests are gone.
  496. session.ref();
  497. if (session[kCurrentStreamCount]++ === 0) {
  498. this._emptySessionCount--;
  499. }
  500. stream.once('close', () => {
  501. if (--session[kCurrentStreamCount] === 0) {
  502. this._emptySessionCount++;
  503. session.unref();
  504. if (this._emptySessionCount > this.maxEmptySessions || session[kGracefullyClosing]) {
  505. session.close();
  506. return;
  507. }
  508. }
  509. if (session.destroyed || session.closed) {
  510. return;
  511. }
  512. if (isFree() && !closeSessionIfCovered(this.sessions[normalizedOptions], session)) {
  513. closeCoveredSessions(this.sessions[normalizedOptions], session);
  514. processListeners();
  515. if (session[kCurrentStreamCount] === 0) {
  516. this._processQueue();
  517. }
  518. }
  519. });
  520. return stream;
  521. };
  522. } catch (error) {
  523. removeFromQueue();
  524. this._sessionCount--;
  525. for (let index = 0; index < listeners.length; index++) {
  526. listeners[index].reject(error);
  527. }
  528. }
  529. };
  530. entry.listeners = listeners;
  531. entry.completed = false;
  532. entry.destroyed = false;
  533. this.queue[normalizedOptions][normalizedOrigin] = entry;
  534. this.queue[normalizedOptions][kLength]++;
  535. this._processQueue();
  536. });
  537. }
  538. request(origin, options, headers, streamOptions) {
  539. return new Promise((resolve, reject) => {
  540. this.getSession(origin, options, [{
  541. reject,
  542. resolve: session => {
  543. try {
  544. const stream = session.request(headers, streamOptions);
  545. // Do not throw before `request(...)` has been awaited
  546. delayAsyncDestroy(stream);
  547. resolve(stream);
  548. } catch (error) {
  549. reject(error);
  550. }
  551. }
  552. }]);
  553. });
  554. }
  555. async createConnection(origin, options) {
  556. return Agent.connect(origin, options);
  557. }
  558. static connect(origin, options) {
  559. options.ALPNProtocols = ['h2'];
  560. const port = origin.port || 443;
  561. const host = origin.hostname;
  562. if (typeof options.servername === 'undefined') {
  563. options.servername = host;
  564. }
  565. const socket = tls.connect(port, host, options);
  566. if (options.socket) {
  567. socket._peername = {
  568. family: undefined,
  569. address: undefined,
  570. port
  571. };
  572. }
  573. return socket;
  574. }
  575. closeEmptySessions(maxCount = Number.POSITIVE_INFINITY) {
  576. let closedCount = 0;
  577. const {sessions} = this;
  578. // eslint-disable-next-line guard-for-in
  579. for (const key in sessions) {
  580. const thisSessions = sessions[key];
  581. for (let index = 0; index < thisSessions.length; index++) {
  582. const session = thisSessions[index];
  583. if (session[kCurrentStreamCount] === 0) {
  584. closedCount++;
  585. session.close();
  586. if (closedCount >= maxCount) {
  587. return closedCount;
  588. }
  589. }
  590. }
  591. }
  592. return closedCount;
  593. }
  594. destroy(reason) {
  595. const {sessions, queue} = this;
  596. // eslint-disable-next-line guard-for-in
  597. for (const key in sessions) {
  598. const thisSessions = sessions[key];
  599. for (let index = 0; index < thisSessions.length; index++) {
  600. thisSessions[index].destroy(reason);
  601. }
  602. }
  603. // eslint-disable-next-line guard-for-in
  604. for (const normalizedOptions in queue) {
  605. const entries = queue[normalizedOptions];
  606. // eslint-disable-next-line guard-for-in
  607. for (const normalizedOrigin in entries) {
  608. entries[normalizedOrigin].destroyed = true;
  609. }
  610. }
  611. // New requests should NOT attach to destroyed sessions
  612. this.queue = {};
  613. this.tlsSessionCache.clear();
  614. }
  615. get emptySessionCount() {
  616. return this._emptySessionCount;
  617. }
  618. get pendingSessionCount() {
  619. return this._sessionCount - this._emptySessionCount;
  620. }
  621. get sessionCount() {
  622. return this._sessionCount;
  623. }
  624. }
  625. Agent.kCurrentStreamCount = kCurrentStreamCount;
  626. Agent.kGracefullyClosing = kGracefullyClosing;
  627. module.exports = {
  628. Agent,
  629. globalAgent: new Agent()
  630. };