comq 0.7.0 → 0.8.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +2 -2
- package/.github/workflows/release.yaml +2 -2
- package/examples/readme.md +0 -4
- package/features/singleton.feature +23 -0
- package/features/steps/.types/context.d.ts +1 -0
- package/features/steps/connection.js +21 -0
- package/features/steps/context.js +12 -5
- package/lerna.json +1 -1
- package/package.json +15 -15
- package/readme.md +27 -18
- package/source/connect.js +27 -15
- package/source/connection.js +1 -1
- package/source/index.js +2 -1
- package/source/singleton.js +68 -0
- package/test/connect.test.js +1 -2
- package/test/join.test.js +58 -0
- package/test/singleton.fixtures.js +21 -0
- package/test/singleton.test.js +113 -0
- package/types/index.d.ts +1 -0
package/.github/dependabot.yml
CHANGED
|
@@ -4,13 +4,13 @@ updates:
|
|
|
4
4
|
directory: "/"
|
|
5
5
|
schedule:
|
|
6
6
|
interval: "weekly"
|
|
7
|
-
target-branch: "
|
|
7
|
+
target-branch: "bumps"
|
|
8
8
|
labels:
|
|
9
9
|
- "Bump"
|
|
10
10
|
- package-ecosystem: "npm"
|
|
11
11
|
directory: "/"
|
|
12
12
|
schedule:
|
|
13
13
|
interval: "daily"
|
|
14
|
-
target-branch: "
|
|
14
|
+
target-branch: "bumps"
|
|
15
15
|
labels:
|
|
16
16
|
- "Bump"
|
package/examples/readme.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
To run examples please start RabbitMQ server with `docker compose up -d` in the package root.
|
|
4
4
|
|
|
5
|
-
> Try to run example first.
|
|
6
|
-
|
|
7
5
|
## RPC
|
|
8
6
|
|
|
9
7
|
Run in two terminals:
|
|
@@ -20,8 +18,6 @@ $ node exampels/rpc/consumer
|
|
|
20
18
|
|
|
21
19
|
[source](rpc/consumer.js)
|
|
22
20
|
|
|
23
|
-
> Try to run consumer first.
|
|
24
|
-
|
|
25
21
|
## Events
|
|
26
22
|
|
|
27
23
|
Run in multiple terminals:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Feature: Singleton connection
|
|
2
|
+
|
|
3
|
+
Scenario: Establishing singleton connection
|
|
4
|
+
Given an active singleton connection
|
|
5
|
+
|
|
6
|
+
Scenario: Establishing sharded singleton connection
|
|
7
|
+
Given an active sharded singleton connection
|
|
8
|
+
|
|
9
|
+
Scenario: Sending request and getting reply
|
|
10
|
+
Given an active singleton connection
|
|
11
|
+
And function replying `add_numbers` queue:
|
|
12
|
+
"""
|
|
13
|
+
({ a, b }) => { return a + b }
|
|
14
|
+
"""
|
|
15
|
+
When the consumer sends the following request to the `add_numbers` queue:
|
|
16
|
+
"""yaml
|
|
17
|
+
a: 1
|
|
18
|
+
b: 2
|
|
19
|
+
"""
|
|
20
|
+
Then the consumer receives the reply:
|
|
21
|
+
"""yaml
|
|
22
|
+
3
|
|
23
|
+
"""
|
|
@@ -26,6 +26,27 @@ Given('an active sharded connection',
|
|
|
26
26
|
await this.connecting
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
Given('an active singleton connection',
|
|
30
|
+
/**
|
|
31
|
+
* @this {comq.features.Context}
|
|
32
|
+
*/
|
|
33
|
+
async function () {
|
|
34
|
+
this.connecting = this.join()
|
|
35
|
+
|
|
36
|
+
await this.connecting
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
Given('an active sharded singleton connection',
|
|
40
|
+
/**
|
|
41
|
+
* @this {comq.features.Context}
|
|
42
|
+
*/
|
|
43
|
+
async function () {
|
|
44
|
+
this.sharded = true
|
|
45
|
+
this.connecting = this.join()
|
|
46
|
+
|
|
47
|
+
await this.connecting
|
|
48
|
+
})
|
|
49
|
+
|
|
29
50
|
When('I attempt to connect to the broker for {number} second(s)',
|
|
30
51
|
/**
|
|
31
52
|
* @param {number} interval
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { World } = require('@cucumber/cucumber')
|
|
4
|
-
const { connect } = require('../../')
|
|
4
|
+
const { connect, join } = require('../../')
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @implements {comq.features.Context}
|
|
@@ -26,7 +26,13 @@ class Context extends World {
|
|
|
26
26
|
async connect (user, password) {
|
|
27
27
|
const urls = this.#urls(user, password)
|
|
28
28
|
|
|
29
|
-
await this.#connect(
|
|
29
|
+
await this.#connect(urls)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async join (user, password) {
|
|
33
|
+
const urls = this.#urls(user, password)
|
|
34
|
+
|
|
35
|
+
await this.#connect(urls, join)
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
async disconnect () {
|
|
@@ -40,13 +46,14 @@ class Context extends World {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
|
-
* @param {
|
|
49
|
+
* @param {string[]} urls
|
|
50
|
+
* @param {comq.connect} [method]
|
|
44
51
|
* @return {Promise<void>}
|
|
45
52
|
*/
|
|
46
|
-
async #connect (
|
|
53
|
+
async #connect (urls, method = connect) {
|
|
47
54
|
if (this.io !== undefined) await this.disconnect()
|
|
48
55
|
|
|
49
|
-
this.io = await
|
|
56
|
+
this.io = await method(...urls)
|
|
50
57
|
this.connected = true
|
|
51
58
|
|
|
52
59
|
for (const event of EVENTS) this.io.diagnose(event, () => (this.events[event] = true))
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-dev.0",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -27,22 +27,22 @@
|
|
|
27
27
|
"main": "source/index.js",
|
|
28
28
|
"types": "types/index.d.ts",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@cucumber/cucumber": "9.1.
|
|
31
|
-
"@toa.io/command": "0.4.0",
|
|
32
|
-
"@toa.io/tomato": "0.4.0",
|
|
33
|
-
"@toa.io/yaml": "0.4.0",
|
|
34
|
-
"@types/amqplib": "0.10.1",
|
|
35
|
-
"@types/jest": "29.5.
|
|
36
|
-
"husky": "8.0.3",
|
|
37
|
-
"jest": "29.5.0",
|
|
38
|
-
"randomstring": "1.
|
|
39
|
-
"snazzy": "9.0.0",
|
|
40
|
-
"standard": "17.
|
|
30
|
+
"@cucumber/cucumber": "^9.1.2",
|
|
31
|
+
"@toa.io/command": "^0.4.0",
|
|
32
|
+
"@toa.io/tomato": "^0.4.0",
|
|
33
|
+
"@toa.io/yaml": "^0.4.0",
|
|
34
|
+
"@types/amqplib": "^0.10.1",
|
|
35
|
+
"@types/jest": "^29.5.2",
|
|
36
|
+
"husky": "^8.0.3",
|
|
37
|
+
"jest": "^29.5.0",
|
|
38
|
+
"randomstring": "^1.3.0",
|
|
39
|
+
"snazzy": "^9.0.0",
|
|
40
|
+
"standard": "^17.1.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@toa.io/generic": "0.4.0",
|
|
44
|
-
"amqplib": "0.10.3",
|
|
45
|
-
"msgpackr": "1.
|
|
43
|
+
"@toa.io/generic": "^0.4.0",
|
|
44
|
+
"amqplib": "^0.10.3",
|
|
45
|
+
"msgpackr": "^1.9.5"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"prepare": "husky install",
|
package/readme.md
CHANGED
|
@@ -173,24 +173,22 @@ The following encoding formats are supported:
|
|
|
173
173
|
- `application/octet-stream`
|
|
174
174
|
- `text/plain`
|
|
175
175
|
|
|
176
|
-
## Flow
|
|
176
|
+
## Flow control
|
|
177
177
|
|
|
178
178
|
When [back pressure](https://www.rabbitmq.com/flow-control.html) is applied to a channel or the
|
|
179
179
|
underlying broker connection is lost, any current and future outgoing messages will be paused.
|
|
180
180
|
Corresponding returned promises will remain in a `pending` state until the pressure is removed or
|
|
181
181
|
the connection is restored.
|
|
182
182
|
|
|
183
|
-
## Connection
|
|
183
|
+
## Connection tolerance
|
|
184
184
|
|
|
185
|
-
When
|
|
186
|
-
attempts will be
|
|
187
|
-
the
|
|
185
|
+
When the established connection is lost, it will be automatically restored.
|
|
186
|
+
Reconnection attempts will be made indefinitely, with intervals increasing up to 30 seconds.
|
|
187
|
+
If the broker rejects the connection, for example, due to access being denied, an exception will be thrown.
|
|
188
|
+
Once reconnected, the topology will be recovered, and any unanswered requests and unconfirmed events will be
|
|
188
189
|
retransmitted.
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
thrown.
|
|
192
|
-
|
|
193
|
-
## Sharded Connection
|
|
191
|
+
## Sharded connection
|
|
194
192
|
|
|
195
193
|
*Send to one, receive from all.*
|
|
196
194
|
|
|
@@ -198,11 +196,11 @@ A sharded connection is a mechanism that uses multiple connections simultaneousl
|
|
|
198
196
|
balancing and mitigate failover scenarios, utilizing a set of broker instances that are **not**
|
|
199
197
|
combined into a cluster.
|
|
200
198
|
|
|
201
|
-
Outgoing messages are sent to a single connection chosen at random from the shard pool. Shards that
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
199
|
+
Outgoing messages are sent to a single connection chosen at random from the shard pool. Shards that lose their
|
|
200
|
+
underlying connection or experience channel [back pressure](#flow-control) on a corresponding channel are removed from
|
|
201
|
+
the pool until the issue is resolved. Pending messages meeting these conditions are immediately routed among the
|
|
202
|
+
remaining shards in the pool. If no shards are available, messages will wait until a shard's connection is
|
|
203
|
+
re-established.
|
|
206
204
|
|
|
207
205
|
Incoming messages are consumed from all shards.
|
|
208
206
|
|
|
@@ -213,7 +211,6 @@ Returns an instance of `IO` once a successful connection to one of the shards is
|
|
|
213
211
|
### Example
|
|
214
212
|
|
|
215
213
|
```javascript
|
|
216
|
-
|
|
217
214
|
const shard0 = 'amqp://developer:secret@localhost:5673'
|
|
218
215
|
const shard1 = 'amqp://developer:secret@localhost:5674'
|
|
219
216
|
|
|
@@ -222,9 +219,21 @@ const io = await connect(shard0, shard1)
|
|
|
222
219
|
// ...
|
|
223
220
|
|
|
224
221
|
await io.close()
|
|
225
|
-
|
|
226
222
|
```
|
|
227
223
|
|
|
224
|
+
## Singleton connection
|
|
225
|
+
|
|
226
|
+
`async join(url: string): IO`
|
|
227
|
+
|
|
228
|
+
Similar to [`connect`](#connect), but it utilizes shared underlying connections.
|
|
229
|
+
|
|
230
|
+
The connection is established once per unique `url` among instances of `IO` created with `join`,
|
|
231
|
+
and it will be closed when the last instance of `IO` using that connection is [disconnected](#disconnection).
|
|
232
|
+
|
|
233
|
+
[Sharded connections](#sharded-connection) are also supported.
|
|
234
|
+
|
|
235
|
+
`async join(...shards: string[]): IO`
|
|
236
|
+
|
|
228
237
|
## Topology
|
|
229
238
|
|
|
230
239
|
Topology is designed to deliver maximum performance while ensuring that the **at least once**
|
|
@@ -258,7 +267,7 @@ when using a single channel or channel pool.
|
|
|
258
267
|
[^1]: The maximum number of messages has been consumed while handlers of those messages have sent
|
|
259
268
|
requests and are expecting replies.
|
|
260
269
|
|
|
261
|
-
### Exchanges and
|
|
270
|
+
### Exchanges and queues
|
|
262
271
|
|
|
263
272
|
- Exchanges and queues for Events, and queues for Requests
|
|
264
273
|
are [durable](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue).
|
|
@@ -294,7 +303,7 @@ See:
|
|
|
294
303
|
| Reply | unlimited | no | exclusive | automatic | no |
|
|
295
304
|
| Event | limited | yes | durable | manual | yes |
|
|
296
305
|
|
|
297
|
-
## Graceful
|
|
306
|
+
## Graceful shutdown
|
|
298
307
|
|
|
299
308
|
### Sealing
|
|
300
309
|
|
package/source/connect.js
CHANGED
|
@@ -2,40 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
const { IO } = require('./io')
|
|
4
4
|
const { Connection } = require('./connection')
|
|
5
|
+
const { SingletonConnection } = require('./singleton')
|
|
5
6
|
const shards = require('./shards')
|
|
6
7
|
|
|
7
8
|
/** @type {comq.connect} */
|
|
8
9
|
const connect = async (...urls) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await connection.open()
|
|
10
|
+
return create(urls, connectionConstructor(Connection))
|
|
11
|
+
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** @type {comq.connect} */
|
|
14
|
+
const join = async (...urls) => {
|
|
15
|
+
return create(urls, connectionConstructor(SingletonConnection))
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* @param {string[]} urls
|
|
18
|
-
* @
|
|
20
|
+
* @param {(urls: string[]) => comq.Connection} constructor
|
|
21
|
+
* @return {Promise<IO>}
|
|
19
22
|
*/
|
|
20
|
-
const create = (urls) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const create = async (urls, constructor) => {
|
|
24
|
+
const connection = constructor(urls)
|
|
25
|
+
|
|
26
|
+
await connection.open()
|
|
27
|
+
|
|
28
|
+
return new IO(connection)
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
const connectionConstructor = (ConnectionClass) =>
|
|
32
|
+
/**
|
|
33
|
+
* @param {string[]} urls
|
|
34
|
+
* @return {comq.Connection}
|
|
35
|
+
*/
|
|
36
|
+
(urls) => {
|
|
37
|
+
if (urls.length === 1) return new ConnectionClass(urls[0])
|
|
38
|
+
else return shardedConnection(urls, ConnectionClass)
|
|
39
|
+
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* @param {string[]} urls
|
|
43
|
+
* @param {function} ConnectionClass
|
|
33
44
|
* @return {comq.Connection}
|
|
34
45
|
*/
|
|
35
|
-
const
|
|
36
|
-
const connections = urls.map(
|
|
46
|
+
const shardedConnection = (urls, ConnectionClass) => {
|
|
47
|
+
const connections = urls.map((url) => new ConnectionClass(url))
|
|
37
48
|
|
|
38
49
|
return new shards.Connection(connections)
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
exports.connect = connect
|
|
53
|
+
exports.join = join
|
package/source/connection.js
CHANGED
|
@@ -79,7 +79,7 @@ class Connection {
|
|
|
79
79
|
else throw exception
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// prevents the process
|
|
82
|
+
// This prevents the process from crashing; 'close' will be emitted next.
|
|
83
83
|
// https://amqp-node.github.io/amqplib/channel_api.html#model_events
|
|
84
84
|
this.#connection.on('error', noop)
|
|
85
85
|
|
package/source/index.js
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Connection } = require('./connection')
|
|
4
|
+
|
|
5
|
+
class SingletonConnection extends Connection {
|
|
6
|
+
/** @type {string} */
|
|
7
|
+
#url
|
|
8
|
+
|
|
9
|
+
/** @type {Promise<void>} */
|
|
10
|
+
#opened = null
|
|
11
|
+
|
|
12
|
+
constructor (url) {
|
|
13
|
+
if (instances.has(url)) return instances.get(url)
|
|
14
|
+
|
|
15
|
+
super(url)
|
|
16
|
+
|
|
17
|
+
this.#url = url
|
|
18
|
+
|
|
19
|
+
instances.set(url, this)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async open () {
|
|
23
|
+
increment(this.#url)
|
|
24
|
+
|
|
25
|
+
if (this.#opened === null) this.#opened = super.open()
|
|
26
|
+
|
|
27
|
+
await this.#opened
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async close () {
|
|
31
|
+
const remainder = decrement(this.#url)
|
|
32
|
+
|
|
33
|
+
if (remainder === 0) await this.#close()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #close () {
|
|
37
|
+
this.#opened = null
|
|
38
|
+
await super.close()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Singletons are evil.
|
|
42
|
+
static __lets_pretend_this_method_doesnt_exist () {
|
|
43
|
+
instances.clear()
|
|
44
|
+
counters.clear()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function increment (url) {
|
|
49
|
+
const value = (counters.get(url) ?? 0) + 1
|
|
50
|
+
|
|
51
|
+
counters.set(url, value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decrement (url) {
|
|
55
|
+
const value = counters.get(url) - 1
|
|
56
|
+
|
|
57
|
+
counters.set(url, value)
|
|
58
|
+
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {Map<string, SingletonConnection>} */
|
|
63
|
+
const instances = new Map()
|
|
64
|
+
|
|
65
|
+
/** @type {Map<string, number>} */
|
|
66
|
+
const counters = new Map()
|
|
67
|
+
|
|
68
|
+
exports.SingletonConnection = SingletonConnection
|
package/test/connect.test.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { generate } = require('randomstring')
|
|
4
4
|
|
|
5
|
-
const { connect } = require('../')
|
|
6
|
-
|
|
7
5
|
jest.mock('../source/io')
|
|
8
6
|
jest.mock('../source/connection')
|
|
9
7
|
jest.mock('../source/shards/connection')
|
|
10
8
|
|
|
9
|
+
const { connect } = require('../')
|
|
11
10
|
const { IO } = require('../source/io')
|
|
12
11
|
|
|
13
12
|
const {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
jest.mock('../source/io')
|
|
6
|
+
jest.mock('../source/singleton')
|
|
7
|
+
jest.mock('../source/shards/connection')
|
|
8
|
+
|
|
9
|
+
const { /** @type {jest.MockedClass<comq.IO>} */ IO } = require('../source/io')
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
/** @type {jest.MockedClass<comq.Connection>} */
|
|
13
|
+
SingletonConnection
|
|
14
|
+
} = require('../source/singleton')
|
|
15
|
+
|
|
16
|
+
const shards = /** @type {{ Connection: jest.MockedClass<comq.Connection>}} */
|
|
17
|
+
require('../source/shards')
|
|
18
|
+
|
|
19
|
+
const { join } = require('../')
|
|
20
|
+
|
|
21
|
+
const url = generate()
|
|
22
|
+
|
|
23
|
+
/** @type {comq.IO} */
|
|
24
|
+
let io
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
jest.clearAllMocks()
|
|
28
|
+
|
|
29
|
+
io = await join(url)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should be', async () => {
|
|
33
|
+
expect(join).toBeInstanceOf(Function)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should return instance of IO', async () => {
|
|
37
|
+
expect(io).toStrictEqual(IO.mock.instances[0])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should create singleton connection', async () => {
|
|
41
|
+
expect(SingletonConnection).toHaveBeenCalled()
|
|
42
|
+
expect(SingletonConnection).toHaveBeenCalledWith(url)
|
|
43
|
+
expect(IO).toHaveBeenCalledWith(SingletonConnection.mock.instances[0])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should create sharded connection', async () => {
|
|
47
|
+
jest.clearAllMocks()
|
|
48
|
+
|
|
49
|
+
const urls = [generate(), generate()]
|
|
50
|
+
|
|
51
|
+
io = await join(...urls)
|
|
52
|
+
|
|
53
|
+
for (const url of urls) expect(SingletonConnection).toHaveBeenCalledWith(url)
|
|
54
|
+
|
|
55
|
+
expect(shards.Connection).toHaveBeenCalledWith(SingletonConnection.mock.instances)
|
|
56
|
+
expect(IO).toHaveBeenCalledWith(shards.Connection.mock.instances[0])
|
|
57
|
+
expect(io).toStrictEqual(IO.mock.instances[0])
|
|
58
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class Connection {
|
|
4
|
+
static ctor = jest.fn(async () => undefined)
|
|
5
|
+
static open = jest.fn(async () => undefined)
|
|
6
|
+
static close = jest.fn(async () => undefined)
|
|
7
|
+
|
|
8
|
+
constructor (...args) {
|
|
9
|
+
Connection.ctor(...args)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async open () {
|
|
13
|
+
await Connection.open()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async close () {
|
|
17
|
+
await Connection.close()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.mock = { Connection }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
const { mock } = require('./singleton.fixtures')
|
|
6
|
+
|
|
7
|
+
jest.mock('../source/connection', () => ({ Connection: mock.Connection }))
|
|
8
|
+
|
|
9
|
+
const { SingletonConnection } = require('../source/singleton')
|
|
10
|
+
|
|
11
|
+
it('should be', async () => {
|
|
12
|
+
expect(SingletonConnection).toBeInstanceOf(Function)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** @type {SingletonConnection} */
|
|
16
|
+
let connection
|
|
17
|
+
|
|
18
|
+
const url = generate()
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks()
|
|
22
|
+
|
|
23
|
+
SingletonConnection.__lets_pretend_this_method_doesnt_exist()
|
|
24
|
+
|
|
25
|
+
connection = new SingletonConnection(url)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should extend Connection', async () => {
|
|
29
|
+
expect(connection).toBeInstanceOf(mock.Connection)
|
|
30
|
+
expect(mock.Connection.ctor).toHaveBeenCalledWith(url)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('connection', () => {
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
await connection.open()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await connection.close()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should open connection', async () => {
|
|
43
|
+
expect(mock.Connection.open).toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should open connection once per url across instances', async () => {
|
|
47
|
+
jest.clearAllMocks()
|
|
48
|
+
|
|
49
|
+
const url = generate()
|
|
50
|
+
const second = new SingletonConnection(url)
|
|
51
|
+
const third = new SingletonConnection(url)
|
|
52
|
+
|
|
53
|
+
await Promise.all([second.open(), third.open()])
|
|
54
|
+
|
|
55
|
+
expect(mock.Connection.open).toHaveBeenCalledTimes(1)
|
|
56
|
+
|
|
57
|
+
const fourth = new SingletonConnection(generate())
|
|
58
|
+
|
|
59
|
+
await fourth.open()
|
|
60
|
+
|
|
61
|
+
expect(mock.Connection.open).toHaveBeenCalledTimes(2)
|
|
62
|
+
|
|
63
|
+
await second.close()
|
|
64
|
+
await third.close()
|
|
65
|
+
await fourth.close()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should close connection', async () => {
|
|
69
|
+
await connection.close()
|
|
70
|
+
|
|
71
|
+
expect(mock.Connection.close).toHaveBeenCalled()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should close connection once', async () => {
|
|
75
|
+
const url = generate()
|
|
76
|
+
const one = new SingletonConnection(url)
|
|
77
|
+
const two = new SingletonConnection(url)
|
|
78
|
+
|
|
79
|
+
await Promise.all([one.open(), two.open()])
|
|
80
|
+
await Promise.all([one.close(), two.close()])
|
|
81
|
+
|
|
82
|
+
expect(mock.Connection.close).toHaveBeenCalledTimes(1)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should not close connection if other instances connected', async () => {
|
|
86
|
+
jest.clearAllMocks()
|
|
87
|
+
const second = new SingletonConnection(url)
|
|
88
|
+
|
|
89
|
+
await second.open()
|
|
90
|
+
await second.close()
|
|
91
|
+
|
|
92
|
+
expect(mock.Connection.close).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should close connection when all instances are disconnected', async () => {
|
|
96
|
+
const second = new SingletonConnection(url)
|
|
97
|
+
|
|
98
|
+
await second.open()
|
|
99
|
+
await second.close()
|
|
100
|
+
await connection.close()
|
|
101
|
+
|
|
102
|
+
expect(mock.Connection.close).toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should reopen connection', async () => {
|
|
106
|
+
const one = new SingletonConnection(url)
|
|
107
|
+
|
|
108
|
+
await connection.close()
|
|
109
|
+
await one.open()
|
|
110
|
+
|
|
111
|
+
expect(mock.Connection.open).toHaveBeenCalledTimes(2)
|
|
112
|
+
})
|
|
113
|
+
})
|
package/types/index.d.ts
CHANGED