codekingpro/portable-devtools
114k
1'use strict'
2
3const { InvalidArgumentError } = require('../core/errors')
4const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('../core/symbols')
5const DispatcherBase = require('./dispatcher-base')
6const Pool = require('./pool')
7const Client = require('./client')
8const util = require('../core/util')
9const createRedirectInterceptor = require('../interceptor/redirect-interceptor')
10
11const kOnConnect = Symbol('onConnect')
12const kOnDisconnect = Symbol('onDisconnect')
13const kOnConnectionError = Symbol('onConnectionError')
14const kMaxRedirections = Symbol('maxRedirections')
15const kOnDrain = Symbol('onDrain')
16const kFactory = Symbol('factory')
17const kOptions = Symbol('options')
18
19function defaultFactory (origin, opts) {
20 return opts && opts.connections === 1
21 ? new Client(origin, opts)
22 : new Pool(origin, opts)
23}
24
25class Agent extends DispatcherBase {
26 constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
27
28 if (typeof factory !== 'function') {
29 throw new InvalidArgumentError('factory must be a function.')
30 }
31
32 if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
33 throw new InvalidArgumentError('connect must be a function or an object')
34 }
35
36 if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
37 throw new InvalidArgumentError('maxRedirections must be a positive number')
38 }
39
40 super(options)
41
42 if (connect && typeof connect !== 'function') {
43 connect = { ...connect }
44 }
45
46 this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent)
47 ? options.interceptors.Agent
48 : [createRedirectInterceptor({ maxRedirections })]
49
50 this[kOptions] = { ...util.deepClone(options), connect }
51 this[kOptions].interceptors = options.interceptors
52 ? { ...options.interceptors }
53 : undefined
54 this[kMaxRedirections] = maxRedirections
55 this[kFactory] = factory
56 this[kClients] = new Map()
57
58 this[kOnDrain] = (origin, targets) => {
59 this.emit('drain', origin, [this, ...targets])
60 }
61
62 this[kOnConnect] = (origin, targets) => {
63 this.emit('connect', origin, [this, ...targets])
64 }
65
66 this[kOnDisconnect] = (origin, targets, err) => {
67 this.emit('disconnect', origin, [this, ...targets], err)
68 }
69
70 this[kOnConnectionError] = (origin, targets, err) => {
71 this.emit('connectionError', origin, [this, ...targets], err)
72 }
73 }
74
75 get [kRunning] () {
76 let ret = 0
77 for (const client of this[kClients].values()) {
78 ret += client[kRunning]
79 }
80 return ret
81 }
82
83 [kDispatch] (opts, handler) {
84 let key
85 if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
86 key = String(opts.origin)
87 } else {
88 throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
89 }
90
91 let dispatcher = this[kClients].get(key)
92
93 if (!dispatcher) {
94 dispatcher = this[kFactory](opts.origin, this[kOptions])
95 .on('drain', this[kOnDrain])
96 .on('connect', this[kOnConnect])
97 .on('disconnect', this[kOnDisconnect])
98 .on('connectionError', this[kOnConnectionError])
99
100 // This introduces a tiny memory leak, as dispatchers are never removed from the map.
101 // TODO(mcollina): remove te timer when the client/pool do not have any more
102 // active connections.
103 this[kClients].set(key, dispatcher)
104 }
105
106 return dispatcher.dispatch(opts, handler)
107 }
108
109 async [kClose] () {
110 const closePromises = []
111 for (const client of this[kClients].values()) {
112 closePromises.push(client.close())
113 }
114 this[kClients].clear()
115
116 await Promise.all(closePromises)
117 }
118
119 async [kDestroy] (err) {
120 const destroyPromises = []
121 for (const client of this[kClients].values()) {
122 destroyPromises.push(client.destroy(err))
123 }
124 this[kClients].clear()
125
126 await Promise.all(destroyPromises)
127 }
128}
129
130module.exports = Agent
131 