comq 0.15.6 → 0.15.8

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.
Files changed (51) hide show
  1. package/.releaserc +1 -8
  2. package/features/recovery.feature +30 -0
  3. package/features/silence.feature +46 -0
  4. package/features/steps/.types/context.d.ts +17 -0
  5. package/features/steps/backpressure.js +1 -1
  6. package/features/steps/broker.js +1 -1
  7. package/features/steps/brokers.js +1 -1
  8. package/features/steps/common.js +1 -1
  9. package/features/steps/connection.js +41 -1
  10. package/features/steps/context.js +12 -2
  11. package/features/steps/events.js +1 -1
  12. package/features/steps/hooks.js +1 -0
  13. package/features/steps/network.js +50 -0
  14. package/features/steps/networks.js +90 -0
  15. package/features/steps/parameters.js +6 -0
  16. package/features/steps/poison.js +1 -1
  17. package/features/steps/rpc.js +1 -1
  18. package/features/steps/tasks.js +1 -1
  19. package/package.json +3 -6
  20. package/readme.md +21 -6
  21. package/source/attributes/lazy.test.js +1 -1
  22. package/source/attributes/track.test.js +3 -3
  23. package/source/connection.js +137 -12
  24. package/source/emitter.js +23 -1
  25. package/source/encoders/index.js +0 -2
  26. package/source/events.js +2 -2
  27. package/source/io.js +6 -5
  28. package/source/shards/channel.js +34 -17
  29. package/source/shards/connection.js +0 -5
  30. package/test/amqplib.mock.js +16 -4
  31. package/test/channel.fixtures.js +1 -1
  32. package/test/channel.test.js +1 -1
  33. package/test/connection.test.js +176 -6
  34. package/test/decode.test.js +0 -13
  35. package/test/emitter.test.js +71 -0
  36. package/test/encode.test.js +0 -9
  37. package/test/encodings.js +1 -1
  38. package/test/helpers.js +77 -0
  39. package/test/io.close.test.js +4 -3
  40. package/test/io.diagnostics.test.js +2 -2
  41. package/test/io.reply.test.js +2 -2
  42. package/test/io.request.test.js +16 -2
  43. package/test/pipeline.test.js +1 -1
  44. package/test/shards/channel.test.js +97 -9
  45. package/test/shards/connection.test.js +4 -3
  46. package/types/diagnostic.d.ts +2 -2
  47. package/types/encoding.d.ts +1 -2
  48. package/types/io.d.ts +2 -0
  49. package/AGENTS.md +0 -35
  50. package/CHANGELOG.md +0 -76
  51. package/source/encoders/msgpack.js +0 -6
@@ -2,7 +2,8 @@
2
2
 
3
3
  const { randomBytes } = require('node:crypto')
4
4
  const { generate } = require('randomstring')
5
- const { promex, sample, immediate, random, each } = require('@toa.io/generic')
5
+ const { Promex } = require('promex')
6
+ const { sample, immediate, random } = require('../helpers')
6
7
 
7
8
  const mock = require('../connection.mock')
8
9
 
@@ -96,6 +97,65 @@ it('should not publish to a lost shard', async () => {
96
97
  expect(channels[1].send).not.toHaveBeenCalled()
97
98
  })
98
99
 
100
+ it('should wait for a shard to recover when all are lost', async () => {
101
+ channel = await create(connections, type)
102
+
103
+ const channels = await getCreatedChannels()
104
+ const buffer = randomBytes(8)
105
+ const label = generate()
106
+ let sent = false
107
+
108
+ for (const connection of connections) {
109
+ const calls = connection.diagnose.mock.calls.filter((call) => call[0] === 'close')
110
+
111
+ for (const call of calls) call[1](new Error('gone'))
112
+ }
113
+
114
+ const sending = channel.send(label, buffer).then(() => { sent = true })
115
+
116
+ await immediate()
117
+
118
+ expect(sent).toStrictEqual(false)
119
+
120
+ for (const chan of channels) expect(chan.send).not.toHaveBeenCalled()
121
+
122
+ const open = connections[0].diagnose.mock.calls.filter((call) => call[0] === 'open')
123
+ const recover = channels[0].diagnose.mock.calls.filter((call) => call[0] === 'recover')
124
+
125
+ for (const call of open) call[1]()
126
+ for (const call of recover) call[1]()
127
+
128
+ await sending
129
+
130
+ expect(channels[0].send).toHaveBeenCalled()
131
+ expect(channels[1].send).not.toHaveBeenCalled()
132
+ })
133
+
134
+ it('should pause when no shard is left to publish to', async () => {
135
+ channel = await create(connections, type)
136
+
137
+ const pause = /** @type {jest.MockedFunction} */ jest.fn()
138
+ const resume = /** @type {jest.MockedFunction} */ jest.fn()
139
+
140
+ channel.diagnose('pause', pause)
141
+ channel.diagnose('resume', resume)
142
+
143
+ lose(connections[0])
144
+
145
+ // the other shard is still reachable
146
+ expect(pause).not.toHaveBeenCalled()
147
+
148
+ lose(connections[1])
149
+
150
+ expect(pause).toHaveBeenCalledTimes(1)
151
+ expect(resume).not.toHaveBeenCalled()
152
+
153
+ restore(connections[1])
154
+
155
+ expect(resume).toHaveBeenCalledTimes(1)
156
+ expect(pause).toHaveBeenCalledTimes(1)
157
+ })
158
+
99
159
  // a lost shard is not benched, so it is used again as soon as it reconnects
100
160
  it('should publish to a recovered shard', async () => {
101
161
  channel = await create(connections, type)
@@ -116,11 +176,11 @@ it('should publish to a recovered shard', async () => {
116
176
  })
117
177
 
118
178
  it('should resolve when one of the connections has created a channel', async () => {
119
- /** @type {toa.generic.Promex[]} */
179
+ /** @type {Promex[]} */
120
180
  const promises = []
121
181
 
122
182
  for (const conn of connections) {
123
- const promise = promex()
183
+ const promise = new Promex()
124
184
 
125
185
  conn.createChannel.mockImplementationOnce(() => promise)
126
186
 
@@ -149,7 +209,7 @@ it('should resolve when one of the connections has created a channel', async ()
149
209
  it('should create failfast channels of given type', async () => {
150
210
  await create(connections, type)
151
211
 
152
- each(connections, (conn, index) => {
212
+ connections.forEach((conn, index) => {
153
213
  expect(conn.createChannel).toHaveBeenCalledWith(type, index)
154
214
  })
155
215
  })
@@ -174,7 +234,7 @@ describe.each(['consume', 'subscribe'])('%s', (method) => {
174
234
  })
175
235
 
176
236
  it(`should not resolve ${method} until pending channels are created`, async () => {
177
- const promise = promex()
237
+ const promise = new Promex()
178
238
 
179
239
  connections[0].createChannel.mockImplementationOnce(() => promise)
180
240
 
@@ -204,7 +264,7 @@ describe.each(['consume', 'subscribe'])('%s', (method) => {
204
264
  })
205
265
 
206
266
  it(`should resolve ${method} without a disconnected shard`, async () => {
207
- const promise = promex()
267
+ const promise = new Promex()
208
268
 
209
269
  connections[0].createChannel.mockImplementationOnce(() => promise)
210
270
  connections[0].connected = false
@@ -226,7 +286,7 @@ describe.each(['consume', 'subscribe'])('%s', (method) => {
226
286
  })
227
287
 
228
288
  it(`should resolve ${method} when a shard disconnects`, async () => {
229
- const promise = promex()
289
+ const promise = new Promex()
230
290
 
231
291
  connections[0].createChannel.mockImplementationOnce(() => promise)
232
292
 
@@ -379,7 +439,7 @@ describe('seal', () => {
379
439
 
380
440
  const channels = await getCreatedChannels()
381
441
  const chan = sample(channels)
382
- const promise = promex()
442
+ const promise = new Promex()
383
443
  let sealed = false
384
444
 
385
445
  chan.seal.mockImplementationOnce(() => promise)
@@ -399,7 +459,7 @@ describe('seal', () => {
399
459
 
400
460
  it('should seal pending channel', async () => {
401
461
  const connection = sample(connections)
402
- const promise = promex()
462
+ const promise = new Promex()
403
463
 
404
464
  connection.createChannel.mockImplementationOnce(() => promise)
405
465
 
@@ -622,6 +682,34 @@ function emitReturn (channel, message) {
622
682
  for (const call of calls) call[1](message)
623
683
  }
624
684
 
685
+ /**
686
+ * @param {jest.MockedObject<comq.Connection>} connection
687
+ * @param {Error} [error]
688
+ */
689
+ function lose (connection, error) {
690
+ emit(connection, 'close', error)
691
+ }
692
+
693
+ /**
694
+ * @param {jest.MockedObject<comq.Connection>} connection
695
+ */
696
+ function restore (connection) {
697
+ emit(connection, 'open')
698
+ }
699
+
700
+ /**
701
+ * @param {jest.MockedObject<comq.Connection>} connection
702
+ * @param {comq.diagnostics.Event} event
703
+ * @param {any[]} args
704
+ */
705
+ function emit (connection, event, ...args) {
706
+ const calls = connection.diagnose.mock.calls.filter((call) => call[0] === event)
707
+
708
+ expect(calls.length).toBeGreaterThan(0)
709
+
710
+ for (const call of calls) call[1](...args)
711
+ }
712
+
625
713
  /**
626
714
  * @return {Promise<jest.MockedObject<comq.Channel>[]>}
627
715
  */
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { generate } = require('randomstring')
4
- const { random, promex, immediate } = require('@toa.io/generic')
4
+ const { Promex } = require('promex')
5
+ const { random, immediate } = require('../helpers')
5
6
 
6
7
  const { Connection } = require('../../source/shards')
7
8
 
@@ -32,11 +33,11 @@ describe('open', () => {
32
33
  it('should resolve when all of the connections are established', async () => {
33
34
  expect.assertions(2)
34
35
 
35
- /** @type {toa.generic.Promex[]} */
36
+ /** @type {Promex[]} */
36
37
  const promises = []
37
38
 
38
39
  for (const conn of connections) {
39
- const promise = promex()
40
+ const promise = new Promex()
40
41
 
41
42
  conn.open.mockImplementation(() => promise)
42
43
  promises.push(promise)
@@ -1,7 +1,7 @@
1
1
  declare namespace comq.diagnostics {
2
2
 
3
- type Event = 'open' | 'close' | 'error' | 'flow' | 'drain' | 'remove' | 'recover' | 'discard' |
4
- 'pause' | 'resume' | 'return'
3
+ type Event = 'open' | 'close' | 'error' | 'reconnect' | 'flow' | 'drain' | 'remove' | 'lost' |
4
+ 'recover' | 'discard' | 'pause' | 'resume' | 'return'
5
5
 
6
6
  interface Diagnosable {
7
7
  diagnose(event: Event, listener: Function): void
@@ -1,8 +1,7 @@
1
1
  declare namespace comq {
2
2
 
3
3
  type Encoding =
4
- 'application/msgpack'
5
- | 'application/json'
4
+ 'application/json'
6
5
  | 'application/octet-stream'
7
6
  | 'text/plain'
8
7
 
package/types/io.d.ts CHANGED
@@ -63,6 +63,8 @@ declare namespace comq {
63
63
 
64
64
  diagnose(event: 'error', listener: (error: Error, index?: number) => void): void
65
65
 
66
+ diagnose(event: 'reconnect', listener: (index?: number) => void): void
67
+
66
68
  diagnose(event: 'flow', listener: (channel: _topology.type, index?: number) => void): void
67
69
 
68
70
  diagnose(event: 'drain', listener: (channel: _topology.type, index?: number) => void): void
package/AGENTS.md DELETED
@@ -1,35 +0,0 @@
1
- # AGENTS.md
2
-
3
- ## Cursor Cloud specific instructions
4
-
5
- ### Project Overview
6
-
7
- ComQ is a Node.js communication library over AMQP (RabbitMQ). It provides RPC, pub/sub, task queues, pipelines, and sharded connections.
8
-
9
- ### Services
10
-
11
- Feature tests start two RabbitMQ instances automatically via Testcontainers (`features/steps/brokers.js`). Credentials: `developer` / `secret`. AMQP ports are assigned dynamically on the host.
12
-
13
- ### Docker
14
-
15
- Feature tests require a running Docker daemon. In nested container environments, start Docker with:
16
-
17
- ```bash
18
- sudo dockerd --exec-opt native.cgroupdriver=cgroupfs &
19
- sudo chmod 666 /var/run/docker.sock
20
- ```
21
-
22
- Do not use `docker compose up` for feature tests — Testcontainers manages broker lifecycle. `docker compose` may fail here because `deploy.resources.limits.memory` requires cgroupv2 memory limits that are unavailable in nested setups.
23
-
24
- ### Commands
25
-
26
- - `npm test` — runs `standard` (lint) + `jest` (300 unit tests, no RabbitMQ required)
27
- - `npm run features` — runs all Cucumber feature tests (starts RabbitMQ via Testcontainers)
28
- - `npm run lint` — runs `standard --fix --verbose | snazzy`
29
-
30
- ### Testing Notes
31
-
32
- - Unit tests (`jest`) use mocks and do NOT require running RabbitMQ.
33
- - Feature tests (`cucumber-js`) start brokers in `BeforeAll` and stop them in `AfterAll`.
34
- - Recovery/connection tests (in `features/connection.feature`, `features/recovery.feature`, `features/shards.feature`) stop/kill broker containers and take 5-15 minutes to complete.
35
- - To run only fast feature tests (no broker stop/start): `npx cucumber-js --fail-fast -t 'not @manual' features/events.feature features/rpc.feature features/tasks.feature features/singleton.feature features/properties.feature features/tasks.shards.feature`
package/CHANGELOG.md DELETED
@@ -1,76 +0,0 @@
1
- ## [0.15.6](https://github.com/toa-io/comq/compare/v0.15.5...v0.15.6) (2026-08-26)
2
-
3
-
4
- ### Bug Fixes
5
-
6
- * do not report shards as lost on shutdown ([abbe0e8](https://github.com/toa-io/comq/commit/abbe0e8e1048509ff2c981b7aa33659c0829e821))
7
- * do not report shards as lost on shutdown ([#248](https://github.com/toa-io/comq/issues/248)) ([b269a5c](https://github.com/toa-io/comq/commit/b269a5cc78d8ca74036290abec234a1e87056e9c))
8
-
9
- ## [0.15.5](https://github.com/toa-io/comq/compare/v0.15.4...v0.15.5) (2026-08-25)
10
-
11
-
12
- ### Bug Fixes
13
-
14
- * recover from a lost connection reliably ([59b1fab](https://github.com/toa-io/comq/commit/59b1fab61d4f27ab6eb608693da6493b5b1f4ae5))
15
- * recover from a lost connection reliably ([#247](https://github.com/toa-io/comq/issues/247)) ([e4701cf](https://github.com/toa-io/comq/commit/e4701cfa384d5451c15d0a6bf53c82ce1da91ac5))
16
-
17
- ## [0.15.4](https://github.com/toa-io/comq/compare/v0.15.3...v0.15.4) (2026-08-16)
18
-
19
-
20
- ### Bug Fixes
21
-
22
- * remove request() timeout ([#246](https://github.com/toa-io/comq/issues/246)) ([d432388](https://github.com/toa-io/comq/commit/d4323889bcad867a870d0ed351b92098f6aad551))
23
-
24
- ## [0.15.3](https://github.com/toa-io/comq/compare/v0.15.2...v0.15.3) (2026-08-04)
25
-
26
-
27
- ### Bug Fixes
28
-
29
- * re-send requests left unanswered by a lost shard ([c534a01](https://github.com/toa-io/comq/commit/c534a0136e5f0189cdb6976cd9cffb9a2ca5a148))
30
- * re-send requests left unanswered by a lost shard ([#242](https://github.com/toa-io/comq/issues/242)) ([c821da8](https://github.com/toa-io/comq/commit/c821da895049bd78a907c6705477ebe74c160e1b))
31
-
32
- ## [0.15.2](https://github.com/toa-io/comq/compare/v0.15.1...v0.15.2) (2026-08-04)
33
-
34
-
35
- ### Bug Fixes
36
-
37
- * report and recover undeliverable replies ([b2d2d0f](https://github.com/toa-io/comq/commit/b2d2d0f11d7714c2d32d3d85894fa78e2203f8c6))
38
- * report and recover undeliverable replies ([#241](https://github.com/toa-io/comq/issues/241)) ([73e5592](https://github.com/toa-io/comq/commit/73e5592762634fbead29d825be68a3da3136729b))
39
-
40
- ## [0.15.1](https://github.com/toa-io/comq/compare/v0.15.0...v0.15.1) (2026-08-02)
41
-
42
-
43
- ### Bug Fixes
44
-
45
- * skip end control when reply stream buffer overflows ([654e046](https://github.com/toa-io/comq/commit/654e046bdcbfbbbbbb789e5bb032bc16564221d1)), closes [#control](https://github.com/toa-io/comq/issues/control) [#reply](https://github.com/toa-io/comq/issues/reply)
46
- * skip end control when reply stream buffer overflows ([#237](https://github.com/toa-io/comq/issues/237)) ([c34d87f](https://github.com/toa-io/comq/commit/c34d87f6d219ce6c8509d989117eadd55a33ca86))
47
-
48
- # [0.15.0](https://github.com/toa-io/comq/compare/v0.14.0...v0.15.0) (2026-08-01)
49
-
50
-
51
- ### Bug Fixes
52
-
53
- * avoid channel recover deadlock after broker restart ([8eeaf2e](https://github.com/toa-io/comq/commit/8eeaf2e12321be71b57913adb6a8fb04bd24d439)), closes [#recovery](https://github.com/toa-io/comq/issues/recovery)
54
- * destroy half-open connections via socket idle watchdog ([9f7ed5f](https://github.com/toa-io/comq/commit/9f7ed5fe50e9ab1857547a6e908185b6b472162f))
55
- * ignore close events from replaced AMQP connections ([c1070ab](https://github.com/toa-io/comq/commit/c1070abeca9eb30a1c0ed07925bc06e530c1d9ce))
56
- * reconnect hang after broker restart ([#235](https://github.com/toa-io/comq/issues/235)) ([be07079](https://github.com/toa-io/comq/commit/be07079863194301d0279e2bdd551260196ce20a))
57
- * retry transient network errors on initial connect ([c436a20](https://github.com/toa-io/comq/commit/c436a2017a2b52d102afa05bc694c9f22faf2065)), closes [#transient](https://github.com/toa-io/comq/issues/transient)
58
- * tolerate empty YAML documents after js-yaml 5 bump ([0a05ecb](https://github.com/toa-io/comq/commit/0a05ecbf744822b3aa884382642ce51a93922dfc))
59
-
60
-
61
- ### Features
62
-
63
- * allow request() to time out pending replies ([ac9e2bd](https://github.com/toa-io/comq/commit/ac9e2bd7eb0834d19d404c84f24548bd5475017f))
64
- * tag container-manipulation features [@heavy](https://github.com/heavy) and add features:fast script ([c0f52ec](https://github.com/toa-io/comq/commit/c0f52ecf72c010e387e853b3daed6884c1cef474))
65
-
66
- # [0.14.0](https://github.com/toa-io/comq/compare/v0.13.0...v0.14.0) (2026-05-24)
67
-
68
-
69
- ### Features
70
-
71
- * **features:** manage RabbitMQ brokers with Testcontainers ([1d9ca59](https://github.com/toa-io/comq/commit/1d9ca591ff458c75caf87ab63224b15942ba7d07))
72
- * **features:** use Testcontainers for RabbitMQ in Gherkin tests ([#219](https://github.com/toa-io/comq/issues/219)) ([a33e66b](https://github.com/toa-io/comq/commit/a33e66bfc357f0f089c728bc60c08f6f24b2c8d1))
73
-
74
- # Changelog
75
-
76
- All notable changes to this project are documented in this file by [semantic-release](https://github.com/semantic-release/semantic-release).
@@ -1,6 +0,0 @@
1
- 'use strict'
2
-
3
- const { pack, unpack } = require('msgpackr')
4
-
5
- exports.encode = pack
6
- exports.decode = unpack