undici
Version:
An HTTP/1.1 client, written from scratch for Node.js
247 lines (205 loc) • 6.24 kB
JavaScript
'use strict'
const Dispatcher = require('./dispatcher')
const Client = require('./client')
const {
ClientClosedError,
InvalidArgumentError,
ClientDestroyedError
} = require('./core/errors')
const FixedQueue = require('./node/fixed-queue')
const util = require('./core/util')
const { kSize, kConnect, kRunning, kUrl, kPending, kBusy } = require('./core/symbols')
const assert = require('assert')
const makeConnect = require('./core/connect')
const kClients = Symbol('clients')
const kNeedDrain = Symbol('needDrain')
const kQueue = Symbol('queue')
const kDestroyed = Symbol('destroyed')
const kClosedPromise = Symbol('closed promise')
const kClosedResolve = Symbol('closed resolve')
const kOptions = Symbol('options')
const kOnDrain = Symbol('onDrain')
const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kConnections = Symbol('connections')
const kFactory = Symbol('factory')
const kQueued = Symbol('queued')
function defaultFactory (origin, opts) {
return new Client(origin, opts)
}
class Pool extends Dispatcher {
constructor (origin, { connections, factory = defaultFactory, [kConnect]: connect, tls, socketPath, ...options } = {}) {
super()
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
throw new InvalidArgumentError('invalid connections')
}
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
if (connect != null && typeof connect !== 'function') {
throw new InvalidArgumentError('connect must be a function')
}
this[kConnections] = connections || null
this[kUrl] = util.parseOrigin(origin)
this[kOptions] = { ...JSON.parse(JSON.stringify(options)), [kConnect]: connect || makeConnect({ tls, socketPath }) }
this[kQueue] = new FixedQueue()
this[kClosedPromise] = null
this[kClosedResolve] = null
this[kDestroyed] = false
this[kClients] = []
this[kNeedDrain] = false
this[kQueued] = 0
this[kFactory] = factory
const pool = this
this[kOnDrain] = function onDrain (url, targets) {
assert(pool[kUrl].origin === url.origin)
const queue = pool[kQueue]
let needDrain = false
while (!needDrain) {
const item = queue.shift()
if (!item) {
break
}
pool[kQueued]--
needDrain = !this.dispatch(item.opts, item.handler)
}
this[kNeedDrain] = needDrain
if (!this[kNeedDrain] && pool[kNeedDrain]) {
pool[kNeedDrain] = false
pool.emit('drain', origin, [pool, ...targets])
}
if (pool[kClosedResolve] && queue.isEmpty()) {
Promise
.all(pool[kClients].map(c => c.close()))
.then(pool[kClosedResolve])
}
}
this[kOnConnect] = (origin, targets) => {
pool.emit('connect', origin, [pool, ...targets])
}
this[kOnDisconnect] = (origin, targets, err) => {
pool.emit('disconnect', origin, [pool, ...targets], err)
}
}
get [kBusy] () {
return this[kNeedDrain]
}
get [kPending] () {
let ret = this[kQueued]
for (const { [kPending]: pending } of this[kClients]) {
ret += pending
}
return ret
}
get [kRunning] () {
let ret = 0
for (const { [kRunning]: running } of this[kClients]) {
ret += running
}
return ret
}
get [kSize] () {
let ret = this[kQueued]
for (const { [kSize]: size } of this[kClients]) {
ret += size
}
return ret
}
get destroyed () {
return this[kDestroyed]
}
get closed () {
return this[kClosedPromise] != null
}
dispatch (opts, handler) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler')
}
try {
if (this[kDestroyed]) {
throw new ClientDestroyedError()
}
if (this[kClosedPromise]) {
throw new ClientClosedError()
}
let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain])
if (!dispatcher) {
if (!this[kConnections] || this[kClients].length < this[kConnections]) {
dispatcher = this[kFactory](this[kUrl], this[kOptions])
.on('drain', this[kOnDrain])
.on('connect', this[kOnConnect])
.on('disconnect', this[kOnDisconnect])
this[kClients].push(dispatcher)
}
}
if (!dispatcher) {
this[kNeedDrain] = true
this[kQueue].push({ opts, handler })
this[kQueued]++
} else if (!dispatcher.dispatch(opts, handler)) {
dispatcher[kNeedDrain] = true
this[kNeedDrain] = this[kConnections] && this[kClients].length === this[kConnections]
}
} catch (err) {
if (typeof handler.onError !== 'function') {
throw new InvalidArgumentError('invalid onError method')
}
handler.onError(err)
}
return !this[kNeedDrain]
}
close (cb) {
try {
if (this[kDestroyed]) {
throw new ClientDestroyedError()
}
if (!this[kClosedPromise]) {
if (this[kQueue].isEmpty()) {
this[kClosedPromise] = Promise.all(this[kClients].map(c => c.close()))
} else {
this[kClosedPromise] = new Promise((resolve) => {
this[kClosedResolve] = resolve
})
}
this[kClosedPromise] = this[kClosedPromise].then(() => {
this[kDestroyed] = true
})
}
if (cb) {
this[kClosedPromise].then(() => cb(null, null))
} else {
return this[kClosedPromise]
}
} catch (err) {
if (cb) {
cb(err)
} else {
return Promise.reject(err)
}
}
}
destroy (err, cb) {
this[kDestroyed] = true
if (typeof err === 'function') {
cb = err
err = null
}
if (!err) {
err = new ClientDestroyedError()
}
while (true) {
const item = this[kQueue].shift()
if (!item) {
break
}
item.handler.onError(err)
}
const promise = Promise.all(this[kClients].map(c => c.destroy(err)))
if (cb) {
promise.then(() => cb(null, null))
} else {
return promise
}
}
}
module.exports = Pool