diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000000..8ff70290cd7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,34 @@ +--- +name: Bug Report +about: Report an issue +title: '' +labels: bug +assignees: '' + +--- + +## Bug Description + + + +## Reproducible By + + + +## Expected Behavior + + + +## Logs & Screenshots + + + +## Environment + + + +### Additional context + + diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000000..0c3a4ff79d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,28 @@ +--- +name: Feature Request +about: Make a suggestion on a feature or improvement for the project +title: '' +labels: enhancement +assignees: '' + +--- + +## This would solve... + + + +## The implementation should look like... + + + +## I have also considered... + + + +## Additional context + + diff --git a/.github/PULL_REQUEST_TEMPLATE/template.md b/.github/PULL_REQUEST_TEMPLATE/template.md new file mode 100644 index 00000000000..cb295de6da6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/template.md @@ -0,0 +1,52 @@ + + +## This relates to... + + + +## Rationale + + + +## Changes + + + +### Features + + + +### Bug Fixes + + + +### Breaking Changes and Deprecations + + + +## Status + +KEY: S = Skipped, x = complete + +- [ ] I have read and agreed to the [Developer's Certificate of Origin][cert] +- [ ] Tested +- [ ] Benchmarked (**optional**) +- [ ] Documented +- [ ] Review ready +- [ ] In review +- [ ] Merge ready + +[cert]: https://github.com/nodejs/undici/blob/main/CONTRIBUTING.md diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000000..31354ec1389 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000000..976abbb0f76 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run coverage diff --git a/index.js b/index.js index 85f00cb70c4..4dd4ba9183a 100644 --- a/index.js +++ b/index.js @@ -2,14 +2,14 @@ const Client = require('./lib/core/client') const errors = require('./lib/core/errors') -const Pool = require('./lib/pool') -const { Agent, request, stream, pipeline, setGlobalAgent } = require('./lib/agent') +const Pool = require('./lib/client-pool') +const { Agent, getGlobalAgent, setGlobalAgent } = require('./lib/agent') +const util = require('./lib/core/util') +const { InvalidArgumentError, InvalidReturnValueError } = require('./lib/core/errors') +const api = require('./lib/api') -Client.prototype.request = require('./lib/client-request') -Client.prototype.stream = require('./lib/client-stream') -Client.prototype.pipeline = require('./lib/client-pipeline') -Client.prototype.upgrade = require('./lib/client-upgrade') -Client.prototype.connect = require('./lib/client-connect') +Object.assign(Client.prototype, api) +Object.assign(Pool.prototype, api) function undici (url, opts) { return new Pool(url, opts) @@ -22,7 +22,30 @@ module.exports.Client = Client module.exports.errors = errors module.exports.Agent = Agent -module.exports.request = request -module.exports.stream = stream -module.exports.pipeline = pipeline module.exports.setGlobalAgent = setGlobalAgent +module.exports.getGlobalAgent = getGlobalAgent + +function dispatchFromAgent (requestType) { + return (url, { agent = getGlobalAgent(), method = 'GET', ...opts } = {}, ...additionalArgs) => { + if (opts.path != null) { + throw new InvalidArgumentError('unsupported opts.path') + } + + const { origin, pathname, search } = util.parseURL(url) + const path = `${pathname || '/'}${search || ''}` + + const client = agent.get(origin) + + if (client && typeof client[requestType] !== 'function') { + throw new InvalidReturnValueError(`Client returned from Agent.get() does not implement method ${requestType}`) + } + + return client[requestType]({ ...opts, method, path }, ...additionalArgs) + } +} + +module.exports.request = dispatchFromAgent('request') +module.exports.stream = dispatchFromAgent('stream') +module.exports.pipeline = dispatchFromAgent('pipeline') +module.exports.connect = dispatchFromAgent('connect') +module.exports.upgrade = dispatchFromAgent('upgrade') diff --git a/lib/agent.js b/lib/agent.js index 5aea6044826..b4a8eff46fc 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,20 +1,30 @@ 'use strict' -const { InvalidArgumentError, InvalidReturnValueError } = require('./core/errors') -const Pool = require('./pool') +const { InvalidArgumentError } = require('./core/errors') +const Pool = require('./client-pool') const Client = require('./core/client') -const util = require('./core/util') -const { kAgentOpts, kAgentCache } = require('./core/symbols') const EventEmitter = require('events') const kOnConnect = Symbol('onConnect') const kOnDisconnect = Symbol('onDisconnect') +const kCache = Symbol('cache') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} class Agent extends EventEmitter { - constructor (opts) { + constructor ({ factory = defaultFactory, ...opts } = {}) { super() - this[kAgentOpts] = opts - this[kAgentCache] = new Map() + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + this[kFactory] = (origin) => factory(origin, opts) + this[kCache] = new Map() const agent = this @@ -25,7 +35,7 @@ class Agent extends EventEmitter { this[kOnDisconnect] = function onDestroy (client, err) { if (this.connected === 0 && this.size === 0) { this.off('disconnect', agent[kOnDisconnect]) - agent[kAgentCache].delete(this.origin) + agent[kCache].delete(this.origin) } agent.emit('disconnect', client, err) @@ -37,19 +47,14 @@ class Agent extends EventEmitter { throw new InvalidArgumentError('Origin must be a non-empty string.') } - const self = this - let pool = self[kAgentCache].get(origin) + let pool = this[kCache].get(origin) if (!pool) { - pool = self[kAgentOpts] && self[kAgentOpts].connections === 1 - ? new Client(origin, self[kAgentOpts]) - : new Pool(origin, self[kAgentOpts]) - - pool + pool = this[kFactory](origin) .on('connect', this[kOnConnect]) .on('disconnect', this[kOnDisconnect]) - self[kAgentCache].set(origin, pool) + this[kCache].set(origin, pool) } return pool @@ -57,7 +62,7 @@ class Agent extends EventEmitter { close () { const closePromises = [] - for (const pool of this[kAgentCache].values()) { + for (const pool of this[kCache].values()) { closePromises.push(pool.close()) } return Promise.all(closePromises) @@ -65,7 +70,7 @@ class Agent extends EventEmitter { destroy () { const destroyPromises = [] - for (const pool of this[kAgentCache].values()) { + for (const pool of this[kCache].values()) { destroyPromises.push(pool.destroy()) } return Promise.all(destroyPromises) @@ -81,32 +86,12 @@ function setGlobalAgent (agent) { globalAgent = agent } -function dispatchFromAgent (requestType) { - return (url, { agent = globalAgent, method = 'GET', ...opts } = {}, ...additionalArgs) => { - if (opts.path != null) { - throw new InvalidArgumentError('unsupported opts.path') - } - - const { origin, pathname, search } = util.parseURL(url) - - const path = `${pathname || '/'}${search || ''}` - - const client = agent.get(origin) - - if (client && typeof client[requestType] !== 'function') { - throw new InvalidReturnValueError(`Client returned from Agent.get() does not implement method ${requestType}`) - } - - return client[requestType]({ ...opts, method, path }, ...additionalArgs) - } +function getGlobalAgent () { + return globalAgent } module.exports = { - request: dispatchFromAgent('request'), - stream: dispatchFromAgent('stream'), - pipeline: dispatchFromAgent('pipeline'), - connect: dispatchFromAgent('connect'), - upgrade: dispatchFromAgent('upgrade'), setGlobalAgent, + getGlobalAgent, Agent } diff --git a/lib/abort-signal.js b/lib/api/abort-signal.js similarity index 94% rename from lib/abort-signal.js rename to lib/api/abort-signal.js index 17fc74057cf..895629aa466 100644 --- a/lib/abort-signal.js +++ b/lib/api/abort-signal.js @@ -1,4 +1,4 @@ -const { RequestAbortedError } = require('./core/errors') +const { RequestAbortedError } = require('../core/errors') const kListener = Symbol('kListener') const kSignal = Symbol('kSignal') diff --git a/lib/client-connect.js b/lib/api/api-connect.js similarity index 89% rename from lib/client-connect.js rename to lib/api/api-connect.js index 2b6fcad5dd7..e6b7503bf2c 100644 --- a/lib/client-connect.js +++ b/lib/api/api-connect.js @@ -1,8 +1,8 @@ 'use strict' -const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') const { AsyncResource } = require('async_hooks') -const util = require('./core/util') +const util = require('../core/util') const { addSignal, removeSignal } = require('./abort-signal') class ConnectHandler extends AsyncResource { @@ -77,17 +77,7 @@ function connect (opts, callback) { try { const connectHandler = new ConnectHandler(opts, callback) - const { - path, - headers, - signal - } = opts - this.dispatch({ - path, - method: 'CONNECT', - headers, - signal - }, connectHandler) + this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler) } catch (err) { process.nextTick(callback, err, { opaque: opts && opts.opaque }) } diff --git a/lib/client-pipeline.js b/lib/api/api-pipeline.js similarity index 93% rename from lib/client-pipeline.js rename to lib/api/api-pipeline.js index 7cd974af823..42b57f6df1c 100644 --- a/lib/client-pipeline.js +++ b/lib/api/api-pipeline.js @@ -9,8 +9,8 @@ const { InvalidArgumentError, InvalidReturnValueError, RequestAbortedError -} = require('./core/errors') -const util = require('./core/util') +} = require('../core/errors') +const util = require('../core/util') const { AsyncResource } = require('async_hooks') const { assert } = require('console') const { addSignal, removeSignal } = require('./abort-signal') @@ -225,21 +225,7 @@ class PipelineHandler extends AsyncResource { function pipeline (opts, handler) { try { const pipelineHandler = new PipelineHandler(opts, handler) - const { - path, - method, - headers, - idempotent, - signal - } = opts - this.dispatch({ - path, - method, - body: pipelineHandler.req, - headers, - idempotent, - signal - }, pipelineHandler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) return pipelineHandler.ret } catch (err) { return new PassThrough().destroy(err) diff --git a/lib/client-request.js b/lib/api/api-request.js similarity index 98% rename from lib/client-request.js rename to lib/api/api-request.js index 7bbde58db08..20397888023 100644 --- a/lib/client-request.js +++ b/lib/api/api-request.js @@ -4,8 +4,8 @@ const { Readable } = require('stream') const { InvalidArgumentError, RequestAbortedError -} = require('./core/errors') -const util = require('./core/util') +} = require('../core/errors') +const util = require('../core/util') const { AsyncResource } = require('async_hooks') const { addSignal, removeSignal } = require('./abort-signal') diff --git a/lib/client-stream.js b/lib/api/api-stream.js similarity index 98% rename from lib/client-stream.js rename to lib/api/api-stream.js index e9ddc86375c..825c9a8c736 100644 --- a/lib/client-stream.js +++ b/lib/api/api-stream.js @@ -5,8 +5,8 @@ const { InvalidArgumentError, InvalidReturnValueError, RequestAbortedError -} = require('./core/errors') -const util = require('./core/util') +} = require('../core/errors') +const util = require('../core/util') const { AsyncResource } = require('async_hooks') const { addSignal, removeSignal } = require('./abort-signal') diff --git a/lib/client-upgrade.js b/lib/api/api-upgrade.js similarity index 88% rename from lib/client-upgrade.js rename to lib/api/api-upgrade.js index 38b1876c4d1..45359410400 100644 --- a/lib/client-upgrade.js +++ b/lib/api/api-upgrade.js @@ -1,8 +1,8 @@ 'use strict' -const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') const { AsyncResource } = require('async_hooks') -const util = require('./core/util') +const util = require('../core/util') const { addSignal, removeSignal } = require('./abort-signal') const assert = require('assert') @@ -79,19 +79,10 @@ function upgrade (opts, callback) { try { const upgradeHandler = new UpgradeHandler(opts, callback) - const { - path, - method, - headers, - signal, - protocol - } = opts this.dispatch({ - path, - method: method || 'GET', - headers, - signal, - upgrade: protocol || 'Websocket' + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' }, upgradeHandler) } catch (err) { process.nextTick(callback, err, { opaque: opts && opts.opaque }) diff --git a/lib/api/index.js b/lib/api/index.js new file mode 100644 index 00000000000..8983a5e746f --- /dev/null +++ b/lib/api/index.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports.request = require('./api-request') +module.exports.stream = require('./api-stream') +module.exports.pipeline = require('./api-pipeline') +module.exports.upgrade = require('./api-upgrade') +module.exports.connect = require('./api-connect') diff --git a/lib/pool.js b/lib/client-pool.js similarity index 96% rename from lib/pool.js rename to lib/client-pool.js index 694388fef0c..dc7749a663f 100644 --- a/lib/pool.js +++ b/lib/client-pool.js @@ -283,10 +283,4 @@ function addCachedSessionToTLSOptions (pool, options) { return tls } -Pool.prototype.request = require('./client-request') -Pool.prototype.stream = require('./client-stream') -Pool.prototype.pipeline = require('./client-pipeline') -Pool.prototype.upgrade = require('./client-upgrade') -Pool.prototype.connect = require('./client-connect') - module.exports = Pool diff --git a/lib/core/symbols.js b/lib/core/symbols.js index 19fb4ca355a..e1ce6405771 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -33,7 +33,5 @@ module.exports = { kTLSSession: Symbol('tls session cache'), kSetTLSSession: Symbol('set tls session'), kHostHeader: Symbol('host header'), - kAgentOpts: Symbol('agent opts'), - kAgentCache: Symbol('agent cache'), kStrictContentLength: Symbol('strict content length') } diff --git a/package.json b/package.json index 9fdb806dd7f..a614c5eec4a 100644 --- a/package.json +++ b/package.json @@ -2,26 +2,20 @@ "name": "undici", "version": "3.3.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", - "main": "index.js", - "types": "index.d.ts", "files": [ "index.(js|d.ts)", "lib", "docs" ], - "scripts": { - "lint": "standard | snazzy", - "test": "tap test/*.js --no-coverage && jest test/jest/test", - "test:typescript": "tsd", - "coverage": "standard | snazzy && tap test/*.js", - "coverage:ci": "npm run coverage -- --coverage-report=lcovonly", - "bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\"", - "serve-website": "docsify serve ." + "homepage": "https://github.com/nodejs/undici#readme", + "bugs": { + "url": "https://github.com/nodejs/undici/issues" }, "repository": { "type": "git", "url": "git+https://github.com/nodejs/undici.git" }, + "license": "MIT", "author": "Matteo Collina ", "contributors": [ { @@ -30,11 +24,18 @@ "author": true } ], - "license": "MIT", - "bugs": { - "url": "https://github.com/nodejs/undici/issues" + "main": "index.js", + "types": "index.d.ts", + "scripts": { + "lint": "standard | snazzy", + "test": "tap test/*.js --no-coverage && jest test/jest/test", + "test:typescript": "tsd", + "coverage": "standard | snazzy && tap test/*.js", + "coverage:ci": "npm run coverage -- --coverage-report=lcovonly", + "bench": "npx concurrently -k -s first \"node benchmarks/server.js\" \"node -e 'setTimeout(() => {}, 1000)' && node benchmarks\"", + "serve-website": "docsify serve .", + "prepare": "husky install" }, - "homepage": "https://github.com/nodejs/undici#readme", "devDependencies": { "@sinonjs/fake-timers": "^6.0.1", "@types/node": "^14.6.2", @@ -43,20 +44,18 @@ "concurrently": "^5.2.0", "docsify-cli": "^4.4.2", "https-pem": "^2.0.0", + "husky": "^5.2.0", "jest": "^26.4.0", "pre-commit": "^1.2.2", "proxy": "^1.0.2", "proxyquire": "^2.0.1", - "sinon": "^9.2.4", "semver": "^5.7.1", + "sinon": "^9.2.4", "snazzy": "^8.0.0", "standard": "^14.3.4", "tap": "^14.10.8", "tsd": "^0.13.1" }, - "pre-commit": [ - "coverage" - ], "tsd": { "directory": "test/types", "compilerOptions": { diff --git a/test/agent.js b/test/agent.js index 0742807b504..8075a051285 100644 --- a/test/agent.js +++ b/test/agent.js @@ -2,7 +2,7 @@ const tap = require('tap') const http = require('http') -const { Agent, request, stream, pipeline, setGlobalAgent } = require('../lib/agent') +const { Agent, request, stream, pipeline, setGlobalAgent } = require('../index') const { PassThrough } = require('stream') const { InvalidArgumentError, InvalidReturnValueError } = require('../lib/core/errors') const { Client, Pool, errors } = require('../index') diff --git a/test/pool.js b/test/pool.js index 3a65d4ce5cb..db966012a1e 100644 --- a/test/pool.js +++ b/test/pool.js @@ -289,7 +289,7 @@ test('backpressure algorithm', (t) => { } } - const Pool = proxyquire('../lib/pool', { + const Pool = proxyquire('../lib/client-pool', { './core/client': FakeClient })