Skip to content

Commit 08d9480

Browse files
authored
Merge pull request #37 from dnlup/fix-url
Full url support
2 parents 00da082 + d101858 commit 08d9480

5 files changed

Lines changed: 161 additions & 86 deletions

File tree

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
> A simple pool manager for [`undici`](https://github.com/nodejs/undici).
88
9-
You might find this module useful if you are using [`undici`](https://github.com/nodejs/undici) and need to manage connections to different and unknown hosts.
9+
You might find this module useful if you use [`undici`](https://github.com/nodejs/undici) and need to manage connections to different and unknown hosts.
1010

1111
`agent-11` controls [`undici`'s](https://github.com/nodejs/undici) pool connections to different hosts. Each time you request a new one, it creates a new pool.
1212
If you don't request this connection after a certain amount of time, `agent-11` will close it.
@@ -20,6 +20,8 @@ If you don't request this connection after a certain amount of time, `agent-11`
2020
- [Usage](#usage)
2121
- [API](#api)
2222
* [Class: `Agent11`](#class-agent11)
23+
+ [Static method: `Agent11.urlToObject(url)`](#static-method-agent11urltoobjecturl)
24+
+ [Static method: `Agent11.getKey(url[, options])`](#static-method-agent11getkeyurl-options)
2325
+ [new `Agent11([options])`](#new-agent11options)
2426
+ [`agent.getConnection(url, [options])`](#agentgetconnectionurl-options)
2527
+ [`agent.close()`](#agentclose)
@@ -84,6 +86,19 @@ agent.destroy(new Error('no more!')).then().catch(console.error)
8486
8587
It manages `undici`'s pool connections.
8688
89+
#### Static method: `Agent11.urlToObject(url)`
90+
91+
* `url` `<string||URL|Object>`: the url to convert.
92+
* Returns: `<Object>` A url-like object with the properties `protocol`, `hostname` and `port`.
93+
94+
#### Static method: `Agent11.getKey(url[, options])`
95+
96+
* `url` `<Object>`: a url-like object.
97+
* `options` `<Object>`: connection options. See [undici documentation](https://github.com/nodejs/undici#new-undiciclienturl-opts).
98+
* Returns: `<string>`: the key that maps the url.
99+
100+
This method creates a key that maps a connection pool to a url.
101+
87102
#### new `Agent11([options])`
88103
89104
* `options` `<Object>`
@@ -131,3 +146,4 @@ $ npm lint
131146
# Create the TOC in the README
132147
$ npm run doc
133148
```
149+

benchmarks/bench.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ const suite = new Benchmark.Suite()
77
const agent = new Agent11({
88
destroyTimeout: 1e9
99
})
10-
const urlString = 'http://localhost:3000/'
11-
const urlObject = new URL('http://localhost:3000')
10+
const urlString = 'http://localhost:3000/some/path'
11+
const urlObject = {
12+
protocol: 'http:',
13+
hostname: 'localhost',
14+
port: 3000,
15+
pathname: '/some/path'
16+
}
17+
const url = new URL('http://localhost:3000/some/path')
18+
19+
// setup the connection first
20+
agent.getConnection(urlString)
1221

1322
/* eslint-disable */
1423
suite
@@ -18,6 +27,9 @@ suite
1827
.add('Agent11.getConnection(<object>)', function getConnectionFromObject() {
1928
const connection = agent.getConnection(urlObject)
2029
})
30+
.add('Agent11.getConnection(<URL>)', function getConnectionFromObject() {
31+
const connection = agent.getConnection(url)
32+
})
2133
.on('cycle', event => console.log(String(event.target)))
2234
.on('complete', function onComplete() {
2335
agent.close().catch()

index.js

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ const {
1212
kClose,
1313
kRefresh,
1414
kStore,
15-
kTimersMap,
16-
kGetKey,
17-
kGetKeyFromString,
18-
kGetKeyFromObject
15+
kTimersMap
1916
} = require('./symbols')
2017

2118
const noop = () => {}
@@ -47,6 +44,43 @@ class Agent11 {
4744
this[kTimersMap] = new Map()
4845
}
4946

47+
static urlToObject (url) {
48+
if (typeof url === 'string' && url.length) {
49+
const match = URL_REG.exec(url)
50+
return {
51+
protocol: match && match[1],
52+
hostname: match && match[2],
53+
port: match && match[3]
54+
}
55+
// perf: this branch has polymorphic inline caches
56+
} else if (typeof url === 'object' && url !== null) {
57+
return {
58+
protocol: url.protocol,
59+
hostname: url.hostname,
60+
port: url.port
61+
}
62+
}
63+
throw new TypeError(`Invalid url, received: ${url}`)
64+
}
65+
66+
static getKey (url, options) {
67+
let key = url.protocol || 'http:'
68+
// perf: this part has polymorphic inline caches
69+
if (key.charAt(key.length - 1) !== ':') {
70+
key += ':'
71+
}
72+
key += url.hostname
73+
if ((typeof url.port === 'string' && url.port.length) || typeof url.port === 'number') {
74+
key += ':'
75+
key += url.port
76+
}
77+
if (options && options.socketPath) {
78+
key += ':'
79+
key += options.socketPath
80+
}
81+
return key
82+
}
83+
5084
[kSetupConnection] (url, options) {
5185
if (this.size === this[kMaxHosts]) {
5286
throw new Error(`Maximum number of ${this[kMaxHosts]} hosts reached`)
@@ -81,46 +115,7 @@ class Agent11 {
81115
return this[kHostsMap].size
82116
}
83117

84-
[kGetKeyFromString] (url) {
85-
const match = URL_REG.exec(url)
86-
return this[kGetKeyFromObject]({
87-
protocol: match && match[1],
88-
hostname: match && match[2],
89-
port: match && match[3]
90-
})
91-
}
92-
93-
[kGetKeyFromObject] (url) {
94-
let key = url.protocol || 'http:'
95-
if (key.charAt(key.length - 1) !== ':') {
96-
key += ':'
97-
}
98-
key += url.hostname
99-
if (typeof url.port === 'string' || typeof url.port === 'number') {
100-
key += ':'
101-
key += url.port
102-
}
103-
return key
104-
}
105-
106-
[kGetKey] (url, options) {
107-
let key = ''
108-
if (typeof url === 'string') {
109-
key = this[kGetKeyFromString](url)
110-
} else if (typeof url === 'object' && url !== null) {
111-
key = this[kGetKeyFromObject](url)
112-
} else {
113-
throw new TypeError(`Can't get key from url: '${url}'`)
114-
}
115-
if (options && options.socketPath) {
116-
key += ':'
117-
key += options.socketPath
118-
}
119-
return key
120-
}
121-
122-
getConnection (url, options) {
123-
const key = this[kGetKey](url, options)
118+
connection (key, url, options) {
124119
if (this[kHostsMap].has(key)) {
125120
const pool = this[kHostsMap].get(key)
126121
this[kRefresh](pool)
@@ -132,6 +127,12 @@ class Agent11 {
132127
}
133128
}
134129

130+
getConnection (url, options) {
131+
url = Agent11.urlToObject(url)
132+
const key = Agent11.getKey(url, options)
133+
return this.connection(key, url, options)
134+
}
135+
135136
close () {
136137
const closing = []
137138
for (const [key, pool] of this[kHostsMap]) {

symbols.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,5 @@ module.exports = {
1010
kClose: Symbol('kClose'),
1111
kRefresh: Symbol('kRefresh'),
1212
kStore: Symbol('kStore'),
13-
kTimersMap: Symbol('kTimersMap'),
14-
kGetKey: Symbol('kGetKey'),
15-
kGetKeyFromString: Symbol('kGetKeyFromString'),
16-
kGetKeyFromObject: Symbol('kGetKeyFromObject')
13+
kTimersMap: Symbol('kTimersMap')
1714
}

test.js

Lines changed: 84 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const { test } = require('tap')
44
const { promisify } = require('util')
55
const Agent11 = require('./')
6-
const { kGetKey } = require('./symbols')
76

87
const sleep = promisify(setTimeout)
98

@@ -41,89 +40,121 @@ test('invalid options', t => {
4140
t.end()
4241
})
4342

44-
test('kGetKey from url and options', t => {
43+
test('Agent11.urlToObject', t => {
44+
let obj = Agent11.urlToObject('http://example.com:3444/some/path?q=1')
45+
t.same(obj, {
46+
protocol: 'http:',
47+
hostname: 'example.com',
48+
port: '3444'
49+
})
50+
obj = Agent11.urlToObject(new URL('http://example.com:3444/some/path?q=1'))
51+
t.same(obj, {
52+
protocol: 'http:',
53+
hostname: 'example.com',
54+
port: '3444'
55+
})
56+
obj = Agent11.urlToObject('https://example.com/some/path?q=1')
57+
t.same(obj, {
58+
protocol: 'https:',
59+
hostname: 'example.com',
60+
port: undefined
61+
})
62+
obj = Agent11.urlToObject('example.com/some/path?q=1')
63+
t.same(obj, {
64+
protocol: undefined,
65+
hostname: 'example.com',
66+
port: undefined
67+
})
68+
t.end()
69+
})
70+
71+
test('Agent11.getKey from url and options', t => {
72+
// TODO: convert this list in sequential manual tests, lists are harder to debug.
4573
const list = [
4674
{
4775
opts: [
48-
new URL('http://localhost:3333')
76+
{
77+
protocol: 'http:',
78+
hostname: 'example.com',
79+
port: '3000'
80+
}
4981
],
50-
expected: 'http:localhost:3333'
82+
expected: 'http:example.com:3000'
5183
},
5284
{
5385
opts: [
54-
{ hostname: 'localhost' }
86+
{
87+
protocol: 'http:',
88+
hostname: 'example.com',
89+
port: 3000
90+
}
5591
],
56-
expected: 'http:localhost'
92+
expected: 'http:example.com:3000'
5793
},
5894
{
5995
opts: [
6096
{
61-
protocol: 'http',
62-
hostname: 'localhost'
97+
protocol: 'http:',
98+
hostname: 'example.com',
99+
port: 0
63100
}
64101
],
65-
expected: 'http:localhost'
102+
expected: 'http:example.com:0'
66103
},
67104
{
68105
opts: [
69-
new URL('https://localhost:3400'),
70-
{
71-
socketPath: '/tmp/agent-11/agent.sock'
72-
}
106+
{ hostname: 'example.com' }
73107
],
74-
expected: 'https:localhost:3400:/tmp/agent-11/agent.sock'
108+
expected: 'http:example.com'
75109
},
76110
{
77111
opts: [
78-
'example.com/1/2/3?some=false'
112+
new URL('http://example.com')
79113
],
80114
expected: 'http:example.com'
81115
},
82116
{
83117
opts: [
84-
'https://example.some.com/1/2/3?some=false',
118+
{
119+
protocol: 'https:',
120+
hostname: 'example.com',
121+
port: 3400
122+
},
85123
{
86124
socketPath: '/tmp/agent-11/agent.sock'
87125
}
88126
],
89-
expected: 'https:example.some.com:/tmp/agent-11/agent.sock'
127+
expected: 'https:example.com:3400:/tmp/agent-11/agent.sock'
90128
},
91129
{
92130
opts: [
93-
'localhost:3000/1/2/3?some=false'
94-
],
95-
expected: 'http:localhost:3000'
96-
},
97-
{
98-
opts: [
99-
'https://localhost:3000/1/2/3?some=false',
131+
new URL('https://example.com:3400'),
100132
{
101133
socketPath: '/tmp/agent-11/agent.sock'
102134
}
103135
],
104-
expected: 'https:localhost:3000:/tmp/agent-11/agent.sock'
136+
expected: 'https:example.com:3400:/tmp/agent-11/agent.sock'
105137
},
106138
{
107139
opts: [
108140
{
109-
hostname: 'some.com',
110-
port: 0
141+
protocol: 'http',
142+
hostname: 'example.com',
143+
pathname: '/some/path?q=1'
111144
}
112145
],
113-
expected: 'http:some.com:0'
146+
expected: 'http:example.com'
114147
},
115148
{
116149
opts: [
117-
'some.com:0'
150+
new URL('http://example.com/some/path?q=1')
118151
],
119-
expected: 'http:some.com:0'
152+
expected: 'http:example.com'
120153
}
121154
]
122155

123-
const agent = new Agent11()
124-
125156
for (const [index, item] of list.entries()) {
126-
const key = agent[kGetKey](...item.opts)
157+
const key = Agent11.getKey(...item.opts)
127158
t.is(key, item.expected, `list item ${index}`)
128159
}
129160
t.end()
@@ -169,6 +200,24 @@ test('getConnection with a unix socket', t => {
169200
t.end()
170201
})
171202

203+
test('getConnection should return the same pool', t => {
204+
const agent = new Agent11()
205+
t.teardown(() => agent.close())
206+
const string = 'http://xyz.xyz/some/path?q=1'
207+
const obj = {
208+
protocol: 'http:',
209+
hostname: 'xyz.xyz'
210+
}
211+
const url = new URL('http://xyz.xyz/some/other/path?q=2')
212+
const options = {
213+
socketPath: '/tmp/agent-11/agent.sock'
214+
}
215+
const pool = agent.getConnection(string, options)
216+
t.is(pool, agent.getConnection(obj, options))
217+
t.is(pool, agent.getConnection(url, options))
218+
t.end()
219+
})
220+
172221
test('getConnection should error if max hosts is reached', t => {
173222
const agent = new Agent11({ maxHosts: 1 })
174223
t.teardown(() => agent.close())
@@ -184,9 +233,9 @@ test('getConnection should error if the url is invalid', t => {
184233
const agent = new Agent11()
185234
t.teardown(() => agent.close())
186235
let error = t.throws(() => agent.getConnection(null))
187-
t.is(error.message, 'Can\'t get key from url: \'null\'')
236+
t.is(error.message, 'Invalid url, received: null')
188237
error = t.throws(() => agent.getConnection(''))
189-
t.is(error.message, 'Invalid URL: ')
238+
t.is(error.message, 'Invalid url, received: ')
190239
error = t.throws(() => agent.getConnection({}))
191240
t.is(error.message, 'invalid protocol')
192241
t.end()

0 commit comments

Comments
 (0)