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/redirectInterceptor') 10const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')() 11 12const kOnConnect = Symbol('onConnect') 13const kOnDisconnect = Symbol('onDisconnect') 14const kOnConnectionError = Symbol('onConnectionError') 15const kMaxRedirections = Symbol('maxRedirections') 16const kOnDrain = Symbol('onDrain') 17const kFactory = Symbol('factory') 18const kFinalizer = Symbol('finalizer') 19const kOptions = Symbol('options') 20 21function defaultFactory (origin, opts) { 22 return opts && opts.connections === 1 23 ? new Client(origin, opts) 24 : new Pool(origin, opts) 25} 26 27class Agent extends DispatcherBase { 28 constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { 29 super() 30 31 if (typeof factory !== 'function') { 32 throw new InvalidArgumentError('factory must be a function.') 33 } 34 35 if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { 36 throw new InvalidArgumentError('connect must be a function or an object') 37 } 38 39 if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { 40 throw new InvalidArgumentError('maxRedirections must be a positive number') 41 } 42 43 if (connect && typeof connect !== 'function') { 44 connect = { ...connect } 45 } 46 47 this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent) 48 ? options.interceptors.Agent 49 : [createRedirectInterceptor({ maxRedirections })] 50 51 this[kOptions] = { ...util.deepClone(options), connect } 52 this[kOptions].interceptors = options.interceptors 53 ? { ...options.interceptors } 54 : undefined 55 this[kMaxRedirections] = maxRedirections 56 this[kFactory] = factory 57 this[kClients] = new Map() 58 this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => { 59 const ref = this[kClients].get(key) 60 if (ref !== undefined && ref.deref() === undefined) { 61 this[kClients].delete(key) 62 } 63 }) 64 65 const agent = this 66 67 this[kOnDrain] = (origin, targets) => { 68 agent.emit('drain', origin, [agent, ...targets]) 69 } 70 71 this[kOnConnect] = (origin, targets) => { 72 agent.emit('connect', origin, [agent, ...targets]) 73 } 74 75 this[kOnDisconnect] = (origin, targets, err) => { 76 agent.emit('disconnect', origin, [agent, ...targets], err) 77 } 78 79 this[kOnConnectionError] = (origin, targets, err) => { 80 agent.emit('connectionError', origin, [agent, ...targets], err) 81 } 82 } 83 84 get [kRunning] () { 85 let ret = 0 86 for (const ref of this[kClients].values()) { 87 const client = ref.deref() 88 /* istanbul ignore next: gc is undeterministic */ 89 if (client) { 90 ret += client[kRunning] 91 } 92 } 93 return ret 94 } 95 96 [kDispatch] (opts, handler) { 97 let key 98 if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { 99 key = String(opts.origin) 100 } else { 101 throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') 102 } 103 104 const ref = this[kClients].get(key) 105 106 let dispatcher = ref ? ref.deref() : null 107 if (!dispatcher) { 108 dispatcher = this[kFactory](opts.origin, this[kOptions]) 109 .on('drain', this[kOnDrain]) 110 .on('connect', this[kOnConnect]) 111 .on('disconnect', this[kOnDisconnect]) 112 .on('connectionError', this[kOnConnectionError]) 113 114 this[kClients].set(key, new WeakRef(dispatcher)) 115 this[kFinalizer].register(dispatcher, key) 116 } 117 118 return dispatcher.dispatch(opts, handler) 119 } 120 121 async [kClose] () { 122 const closePromises = [] 123 for (const ref of this[kClients].values()) { 124 const client = ref.deref() 125 /* istanbul ignore else: gc is undeterministic */ 126 if (client) { 127 closePromises.push(client.close()) 128 } 129 } 130 131 await Promise.all(closePromises) 132 } 133 134 async [kDestroy] (err) { 135 const destroyPromises = [] 136 for (const ref of this[kClients].values()) { 137 const client = ref.deref() 138 /* istanbul ignore else: gc is undeterministic */ 139 if (client) { 140 destroyPromises.push(client.destroy(err)) 141 } 142 } 143 144 await Promise.all(destroyPromises) 145 } 146} 147 148module.exports = Agent 149