htp2-wrapper 2.2.1

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