undici
Version:
An HTTP/1.1 client, written from scratch for Node.js
218 lines (176 loc) • 6.07 kB
JavaScript
'use strict'
const {
ClientClosedError,
InvalidArgumentError,
ClientDestroyedError
} = require('./core/errors')
const { kClients, kRunning } = require('./core/symbols')
const Dispatcher = require('./dispatcher')
const Pool = require('./pool')
const Client = require('./client')
const util = require('./core/util')
const assert = require('assert')
const RedirectHandler = require('./handler/redirect')
const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
const kDestroyed = Symbol('destroyed')
const kClosed = Symbol('closed')
const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kMaxRedirections = Symbol('maxRedirections')
const kOnDrain = Symbol('onDrain')
const kFactory = Symbol('factory')
const kFinalizer = Symbol('finalizer')
const kOptions = Symbol('options')
function defaultFactory (origin, opts) {
return opts && opts.connections === 1
? new Client(origin, opts)
: new Pool(origin, opts)
}
class Agent extends Dispatcher {
constructor ({ factory = defaultFactory, maxRedirections = 0, ...options } = {}) {
super()
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}
this[kOptions] = JSON.parse(JSON.stringify(options))
this[kMaxRedirections] = maxRedirections
this[kFactory] = factory
this[kClients] = new Map()
this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => {
const ref = this[kClients].get(key)
if (ref !== undefined && ref.deref() === undefined) {
this[kClients].delete(key)
}
})
this[kClosed] = false
this[kDestroyed] = false
const agent = this
this[kOnDrain] = (origin, targets) => {
agent.emit('drain', origin, [agent, ...targets])
}
this[kOnConnect] = (origin, targets) => {
agent.emit('connect', origin, [agent, ...targets])
}
this[kOnDisconnect] = (origin, targets, err) => {
agent.emit('disconnect', origin, [agent, ...targets], err)
}
}
get [kRunning] () {
let ret = 0
for (const ref of this[kClients].values()) {
const client = ref.deref()
if (client) {
ret += client[kRunning]
}
}
return ret
}
dispatch (opts, handler) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler')
}
try {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('opts must be a object.')
}
if (typeof opts.origin !== 'string' || opts.origin === '') {
throw new InvalidArgumentError('opts.origin must be a non-empty string.')
}
if (this[kDestroyed]) {
throw new ClientDestroyedError()
}
if (this[kClosed]) {
throw new ClientClosedError()
}
const ref = this[kClients].get(opts.origin)
let dispatcher = ref ? ref.deref() : null
if (!dispatcher) {
dispatcher = this[kFactory](opts.origin, this[kOptions])
.on('connect', this[kOnConnect])
.on('disconnect', this[kOnDisconnect])
.on('drain', this[kOnDrain])
this[kClients].set(opts.origin, new WeakRef(dispatcher))
this[kFinalizer].register(dispatcher, opts.origin)
}
const { maxRedirections = this[kMaxRedirections] } = opts
if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}
if (!maxRedirections) {
return dispatcher.dispatch(opts, handler)
}
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
// so that it can be dispatched again?
// TODO (fix): Do we need 100-expect support to provide a way to do this properly?
return dispatcher.dispatch(opts, handler)
}
/* istanbul ignore next */
if (util.isStream(opts.body)) {
opts.body
.on('data', function () {
assert(false)
})
}
return dispatcher.dispatch(opts, new RedirectHandler(this, opts, handler))
} catch (err) {
if (typeof handler.onError !== 'function') {
throw new InvalidArgumentError('invalid onError method')
}
handler.onError(err)
}
}
get closed () {
return this[kClosed]
}
get destroyed () {
return this[kDestroyed]
}
close (callback) {
if (callback != null && typeof callback !== 'function') {
throw new InvalidArgumentError('callback must be a function')
}
this[kClosed] = true
const closePromises = []
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore else: gc is undeterministic */
if (client) {
closePromises.push(client.close())
}
}
if (!callback) {
return Promise.all(closePromises)
}
// Should never error.
Promise.all(closePromises).then(() => process.nextTick(callback))
}
destroy (err, callback) {
if (typeof err === 'function') {
callback = err
err = null
}
if (callback != null && typeof callback !== 'function') {
throw new InvalidArgumentError('callback must be a function')
}
this[kClosed] = true
this[kDestroyed] = true
const destroyPromises = []
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore else: gc is undeterministic */
if (client) {
destroyPromises.push(client.destroy(err))
}
}
if (!callback) {
return Promise.all(destroyPromises)
}
// Should never error.
Promise.all(destroyPromises).then(() => process.nextTick(callback))
}
}
module.exports = Agent