comq 0.16.1 → 0.17.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 +5 -0
- package/features/events.routed.feature +19 -0
- package/features/steps/events.routed.js +55 -0
- package/package.json +1 -1
- package/readme.md +44 -2
- package/source/.io/ReplyPipe.js +57 -14
- package/source/.io/ReplyStream.js +64 -10
- package/source/.io/const.js +9 -1
- package/source/.io/createReplyEmitter.js +84 -9
- package/source/attributes/lazy.js +65 -25
- package/source/attributes/lazy.test.js +39 -0
- package/source/attributes/recall.js +17 -0
- package/source/attributes/recall.test.js +43 -0
- package/source/channel.js +82 -7
- package/source/connection.js +16 -8
- package/source/io.js +94 -31
- package/source/pipeline.js +12 -4
- package/source/shards/connection.js +4 -0
- package/test/ReplyPipe.test.js +170 -0
- package/test/ReplyStream.test.js +82 -4
- package/test/channel.routed.test.js +119 -0
- package/test/channel.test.js +58 -0
- package/test/connection.mock.js +5 -1
- package/test/connection.test.js +34 -2
- package/test/io.diagnostics.test.js +11 -0
- package/test/io.route.test.js +94 -0
- package/test/memory.test.js +273 -0
- package/test/pipeline.test.js +21 -0
- package/test/shards/connection.test.js +15 -0
- package/types/channel.d.ts +6 -0
- package/types/connection.d.ts +2 -0
- package/types/io.d.ts +28 -2
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Runs the initializers once per distinct set of arguments before the method.
|
|
5
|
+
*
|
|
6
|
+
* The record of what has been initialized is kept per context, so that a context
|
|
7
|
+
* that is dropped takes its record with it, and it is looked up by argument rather
|
|
8
|
+
* than scanned — a channel publishing to a thousand queues asks for the thousandth
|
|
9
|
+
* as fast as for the first.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} context
|
|
12
|
+
* @param {Function | Function[]} initializers
|
|
13
|
+
* @param {(...args: unknown[]) => Promise<unknown>} method
|
|
14
|
+
*/
|
|
3
15
|
function lazy (context, initializers, method) {
|
|
4
|
-
if (context[LOCK] === undefined) context[LOCK] = Symbol('methods locking key')
|
|
5
|
-
|
|
6
16
|
if (!Array.isArray(initializers)) initializers = [initializers]
|
|
7
17
|
|
|
8
18
|
return async function (...args) {
|
|
@@ -24,7 +34,7 @@ async function call (context, initializers, args) {
|
|
|
24
34
|
let override
|
|
25
35
|
|
|
26
36
|
for (const init of initializers) {
|
|
27
|
-
const result = await
|
|
37
|
+
const result = await lock(context, init, args)
|
|
28
38
|
|
|
29
39
|
if (result !== undefined) override = result
|
|
30
40
|
}
|
|
@@ -33,46 +43,76 @@ async function call (context, initializers, args) {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
/**
|
|
46
|
+
* An initializer expecting N arguments is run once per distinct N leading
|
|
47
|
+
* arguments, which are the keys of a trie of Maps N levels deep.
|
|
48
|
+
*
|
|
36
49
|
* @param {object} context
|
|
37
|
-
* @param {
|
|
38
|
-
* @param {
|
|
39
|
-
* @returns {Promise}
|
|
50
|
+
* @param {(...args: unknown[]) => Promise<unknown>} init
|
|
51
|
+
* @param {unknown[]} args
|
|
52
|
+
* @returns {Promise<unknown>}
|
|
40
53
|
*/
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
|
|
54
|
+
function lock (context, init, args) {
|
|
55
|
+
const arity = init.length
|
|
56
|
+
|
|
57
|
+
let node = table(context, init)
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < arity - 1; i++) node = branch(node, args[i])
|
|
60
|
+
|
|
61
|
+
const key = arity === 0 ? NONE : args[arity - 1]
|
|
62
|
+
const found = node.get(key)
|
|
63
|
+
|
|
64
|
+
if (found !== undefined) return found
|
|
65
|
+
|
|
66
|
+
const promise = init.apply(context, args.slice(0, arity))
|
|
67
|
+
|
|
68
|
+
node.set(key, promise)
|
|
44
69
|
|
|
45
|
-
return
|
|
70
|
+
return promise
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
/**
|
|
49
74
|
* @param {object} context
|
|
50
|
-
* @param {
|
|
51
|
-
* @
|
|
52
|
-
* @param {Symbol} key
|
|
53
|
-
* @returns {Promise<unknown>}
|
|
75
|
+
* @param {Function} init
|
|
76
|
+
* @return {Map<unknown, unknown>}
|
|
54
77
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
78
|
+
function table (context, init) {
|
|
79
|
+
let tables = TABLES.get(context)
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
if (tables === undefined) {
|
|
82
|
+
tables = new Map()
|
|
83
|
+
TABLES.set(context, tables)
|
|
84
|
+
}
|
|
60
85
|
|
|
61
|
-
|
|
86
|
+
return branch(tables, init)
|
|
87
|
+
}
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @param {Map<unknown, unknown>} node
|
|
91
|
+
* @param {unknown} key
|
|
92
|
+
* @return {Map<unknown, unknown>}
|
|
93
|
+
*/
|
|
94
|
+
function branch (node, key) {
|
|
95
|
+
let next = node.get(key)
|
|
65
96
|
|
|
66
|
-
|
|
97
|
+
if (next === undefined) {
|
|
98
|
+
next = new Map()
|
|
99
|
+
node.set(key, next)
|
|
100
|
+
}
|
|
67
101
|
|
|
68
|
-
return
|
|
102
|
+
return /** @type {Map<unknown, unknown>} */ next
|
|
69
103
|
}
|
|
70
104
|
|
|
105
|
+
/**
|
|
106
|
+
* @param {object} context
|
|
107
|
+
*/
|
|
71
108
|
function reset (context) {
|
|
72
|
-
context
|
|
109
|
+
TABLES.delete(context)
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
|
|
112
|
+
/** @type {WeakMap<object, Map<Function, Map<unknown, unknown>>>} */
|
|
113
|
+
const TABLES = new WeakMap()
|
|
114
|
+
|
|
115
|
+
const NONE = Symbol('no arguments')
|
|
76
116
|
|
|
77
117
|
lazy.reset = reset
|
|
78
118
|
|
|
@@ -297,3 +297,42 @@ describe('reset', () => {
|
|
|
297
297
|
expect(initialize2).toHaveBeenCalledTimes(2)
|
|
298
298
|
})
|
|
299
299
|
})
|
|
300
|
+
|
|
301
|
+
class Exposed {
|
|
302
|
+
do = lazy(this, this.initialize, async () => undefined)
|
|
303
|
+
|
|
304
|
+
async initialize (_) {}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// an initializer is a method shared by every instance of its class, so a record
|
|
308
|
+
// kept on it would outlive the instances it was made for
|
|
309
|
+
it('should keep no record on the initializer or the context', async () => {
|
|
310
|
+
const instance = new Exposed()
|
|
311
|
+
|
|
312
|
+
await instance.do(generate())
|
|
313
|
+
lazy.reset(instance)
|
|
314
|
+
await instance.do(generate())
|
|
315
|
+
|
|
316
|
+
expect(Object.getOwnPropertySymbols(Exposed.prototype.initialize)).toHaveLength(0)
|
|
317
|
+
expect(Object.getOwnPropertySymbols(instance)).toHaveLength(0)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should tell arguments apart by identity', async () => {
|
|
321
|
+
let calls = 0
|
|
322
|
+
|
|
323
|
+
class Counted {
|
|
324
|
+
do = lazy(this, this.initialize, async () => undefined)
|
|
325
|
+
|
|
326
|
+
async initialize (_) {
|
|
327
|
+
calls++
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const instance = new Counted()
|
|
332
|
+
|
|
333
|
+
await instance.do(undefined)
|
|
334
|
+
await instance.do('undefined')
|
|
335
|
+
await instance.do(undefined)
|
|
336
|
+
|
|
337
|
+
expect(calls).toStrictEqual(2)
|
|
338
|
+
})
|
|
@@ -36,7 +36,24 @@ async function replay (context) {
|
|
|
36
36
|
await Promise.all(promises)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Forgets the recorded calls, and the arguments they hold on to.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} context
|
|
43
|
+
*/
|
|
44
|
+
function reset (context) {
|
|
45
|
+
const methods = context[METHODS]
|
|
46
|
+
|
|
47
|
+
if (methods === undefined) return
|
|
48
|
+
|
|
49
|
+
for (const method of methods) method[CALLS] = undefined
|
|
50
|
+
|
|
51
|
+
context[METHODS] = undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
const METHODS = Symbol('context methods')
|
|
40
55
|
const CALLS = Symbol('method calls')
|
|
41
56
|
|
|
57
|
+
recall.reset = reset
|
|
58
|
+
|
|
42
59
|
exports.recall = recall
|
|
@@ -90,3 +90,46 @@ it('should not re-call those thrown exceptions', async () => {
|
|
|
90
90
|
|
|
91
91
|
expect(method).toHaveBeenCalledTimes(3)
|
|
92
92
|
})
|
|
93
|
+
|
|
94
|
+
describe('reset', () => {
|
|
95
|
+
it('should be', async () => {
|
|
96
|
+
expect(recall.reset).toBeDefined()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should forget recorded calls', async () => {
|
|
100
|
+
const context = {}
|
|
101
|
+
const func = recall(context, method)
|
|
102
|
+
|
|
103
|
+
await func(generate())
|
|
104
|
+
|
|
105
|
+
recall.reset(context)
|
|
106
|
+
method.mockClear()
|
|
107
|
+
|
|
108
|
+
await recall(context)
|
|
109
|
+
|
|
110
|
+
expect(method).not.toHaveBeenCalled()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should record again after reset', async () => {
|
|
114
|
+
const context = {}
|
|
115
|
+
const func = recall(context, method)
|
|
116
|
+
|
|
117
|
+
await func(generate())
|
|
118
|
+
|
|
119
|
+
recall.reset(context)
|
|
120
|
+
|
|
121
|
+
const args = [generate()]
|
|
122
|
+
|
|
123
|
+
await func(...args)
|
|
124
|
+
method.mockClear()
|
|
125
|
+
|
|
126
|
+
await recall(context)
|
|
127
|
+
|
|
128
|
+
expect(method).toHaveBeenCalledTimes(1)
|
|
129
|
+
expect(method).toHaveBeenCalledWith(...args)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should not throw on a context without records', async () => {
|
|
133
|
+
expect(() => recall.reset({})).not.toThrow()
|
|
134
|
+
})
|
|
135
|
+
})
|
package/source/channel.js
CHANGED
|
@@ -44,17 +44,22 @@ class Channel {
|
|
|
44
44
|
|
|
45
45
|
#closed = false
|
|
46
46
|
|
|
47
|
+
/** @type {(channel: comq.Channel) => void} */
|
|
48
|
+
#release
|
|
49
|
+
|
|
47
50
|
/**
|
|
48
51
|
* @param {comq.amqp.Connection} connection
|
|
49
52
|
* @param {comq.Topology} topology
|
|
50
|
-
* @param {number} index
|
|
53
|
+
* @param {number} [index]
|
|
54
|
+
* @param {(channel: comq.Channel) => void} [release] called once the channel is given back
|
|
51
55
|
*/
|
|
52
|
-
constructor (connection, topology, index) {
|
|
56
|
+
constructor (connection, topology, index, release = noop) {
|
|
53
57
|
this.index = index
|
|
54
58
|
|
|
55
59
|
this.#connection = connection
|
|
56
60
|
this.#topology = topology
|
|
57
61
|
this.#failfast = index !== undefined
|
|
62
|
+
this.#release = release
|
|
58
63
|
|
|
59
64
|
if (this.#failfast) failsafe.disable(this.send, this.publish)
|
|
60
65
|
}
|
|
@@ -63,6 +68,9 @@ class Channel {
|
|
|
63
68
|
if (this.#topology.confirms) this.#channel = await this.#connection.createConfirmChannel()
|
|
64
69
|
else this.#channel = await this.#connection.createChannel()
|
|
65
70
|
|
|
71
|
+
// the consumers of the previous channel went down with it, their tags mean nothing here
|
|
72
|
+
this.#tags = []
|
|
73
|
+
|
|
66
74
|
await this.#channel.prefetch(this.#topology.prefetch)
|
|
67
75
|
|
|
68
76
|
this.#channel.on('drain', this.#unpause)
|
|
@@ -93,6 +101,24 @@ class Channel {
|
|
|
93
101
|
if (!this.#sealed) await this.#consume(queue, callback)
|
|
94
102
|
})))
|
|
95
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Consumes a named queue bound to a direct exchange under a routing key, where `subscribe`
|
|
106
|
+
* binds to a fanout and takes everything published to it.
|
|
107
|
+
*/
|
|
108
|
+
bound = recall(this,
|
|
109
|
+
failsafe(this, this.#recover,
|
|
110
|
+
lazy(this, [this.#assertRouted, this.#assertKeyedQueue],
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} exchange
|
|
113
|
+
* @param {string} queue
|
|
114
|
+
* @param {string} key
|
|
115
|
+
* @param {comq.channels.Consumer} callback
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async (exchange, queue, key, callback) => {
|
|
119
|
+
if (!this.#sealed) await this.#consume(queue, callback)
|
|
120
|
+
})))
|
|
121
|
+
|
|
96
122
|
send = failsafe(this, this.#recover,
|
|
97
123
|
lazy(this, this.#assertQueue,
|
|
98
124
|
/**
|
|
@@ -115,6 +141,21 @@ class Channel {
|
|
|
115
141
|
await this.#publish(exchange, DEFAULT, buffer, options)
|
|
116
142
|
}))
|
|
117
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Publishes to a direct exchange under a routing key, where `publish` fans out.
|
|
146
|
+
*/
|
|
147
|
+
route = failsafe(this, this.#recover,
|
|
148
|
+
lazy(this, this.#assertRouted,
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} exchange
|
|
151
|
+
* @param {string} key
|
|
152
|
+
* @param {Buffer} buffer
|
|
153
|
+
* @param {comq.amqp.options.Publish} [options]
|
|
154
|
+
*/
|
|
155
|
+
async (exchange, key, buffer, options) => {
|
|
156
|
+
await this.#publish(exchange, key, buffer, options)
|
|
157
|
+
}))
|
|
158
|
+
|
|
118
159
|
async fire (queue, buffer, options) {
|
|
119
160
|
try {
|
|
120
161
|
await this.#publish(DEFAULT, queue, buffer, options)
|
|
@@ -141,6 +182,8 @@ class Channel {
|
|
|
141
182
|
|
|
142
183
|
// a channel that went down with its connection is the outcome this asks for
|
|
143
184
|
await this.#channel?.close().catch(noop)
|
|
185
|
+
|
|
186
|
+
this.#release(this)
|
|
144
187
|
}
|
|
145
188
|
|
|
146
189
|
async seal () {
|
|
@@ -149,6 +192,10 @@ class Channel {
|
|
|
149
192
|
const cancellations = this.#tags.map((tag) => this.#channel.cancel(tag))
|
|
150
193
|
|
|
151
194
|
await Promise.all(cancellations).catch(noop) // won't recover anyway
|
|
195
|
+
|
|
196
|
+
// a sealed channel is not going to consume again, so what it has consumed
|
|
197
|
+
// and the callbacks it was given are of no use anymore
|
|
198
|
+
recall.reset(this)
|
|
152
199
|
}
|
|
153
200
|
|
|
154
201
|
diagnose (event, listener) {
|
|
@@ -216,6 +263,33 @@ class Channel {
|
|
|
216
263
|
await this.#channel.assertExchange(exchange, 'fanout', options)
|
|
217
264
|
}
|
|
218
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A routed exchange is `direct`: a message reaches the queues bound under the key it was
|
|
268
|
+
* published with, and none of the others.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} exchange
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
async #assertRouted (exchange) {
|
|
274
|
+
/** @type {comq.amqp.options.Exchange} */
|
|
275
|
+
const options = { durable: this.#topology.durable }
|
|
276
|
+
|
|
277
|
+
await this.#channel.assertExchange(exchange, 'direct', options)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {string} exchange
|
|
282
|
+
* @param {string} queue
|
|
283
|
+
* @param {string} key
|
|
284
|
+
* @returns {Promise<string[]>}
|
|
285
|
+
*/
|
|
286
|
+
async #assertKeyedQueue (exchange, queue, key) {
|
|
287
|
+
queue = (await this.#assertQueue(queue))[0]
|
|
288
|
+
await this.#channel.bindQueue(queue, exchange, key)
|
|
289
|
+
|
|
290
|
+
return [exchange, queue, key]
|
|
291
|
+
}
|
|
292
|
+
|
|
219
293
|
/**
|
|
220
294
|
*
|
|
221
295
|
* @param {string} exchange
|
|
@@ -260,12 +334,12 @@ class Channel {
|
|
|
260
334
|
*/
|
|
261
335
|
#confirmation () {
|
|
262
336
|
const confirmation = new Promex()
|
|
337
|
+
const settled = () => this.#confirmations.delete(confirmation)
|
|
263
338
|
|
|
264
339
|
this.#confirmations.add(confirmation)
|
|
265
340
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.finally(() => this.#confirmations.delete(confirmation))
|
|
341
|
+
// one derived promise per publish, and a rejection handled with it
|
|
342
|
+
confirmation.then(settled, settled)
|
|
269
343
|
|
|
270
344
|
return confirmation
|
|
271
345
|
}
|
|
@@ -372,10 +446,11 @@ class Channel {
|
|
|
372
446
|
* @param {comq.amqp.Connection} connection
|
|
373
447
|
* @param {comq.Topology} topology
|
|
374
448
|
* @param {number} [index]
|
|
449
|
+
* @param {(channel: comq.Channel) => void} [release]
|
|
375
450
|
* @return {Promise<comq.Channel>}
|
|
376
451
|
*/
|
|
377
|
-
async function create (connection, topology, index) {
|
|
378
|
-
const channel = new Channel(connection, topology, index)
|
|
452
|
+
async function create (connection, topology, index, release) {
|
|
453
|
+
const channel = new Channel(connection, topology, index, release)
|
|
379
454
|
|
|
380
455
|
await channel.create()
|
|
381
456
|
|
package/source/connection.js
CHANGED
|
@@ -20,8 +20,8 @@ class Connection {
|
|
|
20
20
|
/** @type {comq.amqp.Connection} */
|
|
21
21
|
#connection
|
|
22
22
|
|
|
23
|
-
/** @type {comq.Channel
|
|
24
|
-
#channels =
|
|
23
|
+
/** @type {Set<comq.Channel>} */
|
|
24
|
+
#channels = new Set()
|
|
25
25
|
|
|
26
26
|
/** @type {Promex} */
|
|
27
27
|
#recovery = new Promex()
|
|
@@ -90,10 +90,13 @@ class Connection {
|
|
|
90
90
|
if (this.#connection === undefined) await this.#recovery
|
|
91
91
|
|
|
92
92
|
const topology = presets[type]
|
|
93
|
-
const channel = await channels.create(this.#connection, topology, index)
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// a closed channel held here would hold its IO, and a shared connection is
|
|
95
|
+
// in no hurry to make another channel that would have swept it out
|
|
96
|
+
const release = (channel) => this.#channels.delete(channel)
|
|
97
|
+
const channel = await channels.create(this.#connection, topology, index, release)
|
|
98
|
+
|
|
99
|
+
this.#channels.add(channel)
|
|
97
100
|
|
|
98
101
|
return channel
|
|
99
102
|
})
|
|
@@ -102,6 +105,10 @@ class Connection {
|
|
|
102
105
|
this.#diagnostics.on(event, listener)
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
forget (event, listener) {
|
|
109
|
+
this.#diagnostics.off(event, listener)
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
#open = async (retry) => {
|
|
106
113
|
if (this.#closed) return
|
|
107
114
|
|
|
@@ -134,9 +141,10 @@ class Connection {
|
|
|
134
141
|
this.#diagnostics.emit('open')
|
|
135
142
|
|
|
136
143
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
for (const channel of this.#channels) {
|
|
145
|
+
if (channel.closed) this.#channels.delete(channel)
|
|
146
|
+
else await channel.recover(connection)
|
|
147
|
+
}
|
|
140
148
|
} catch (exception) {
|
|
141
149
|
this.#diagnostics.emit('error', exception)
|
|
142
150
|
this.#drop(connection)
|