comq 0.9.0 → 0.10.1

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 (60) hide show
  1. package/.github/dependabot.yml +2 -2
  2. package/.github/workflows/codeql.yml +1 -1
  3. package/.github/workflows/release.yaml +14 -15
  4. package/docs/reply-stream-topology-dark.jpg +0 -0
  5. package/docs/reply-stream-topology-light.jpg +0 -0
  6. package/examples/readme.md +19 -3
  7. package/examples/streams/consumer.js +36 -0
  8. package/examples/streams/producer.js +44 -0
  9. package/features/backpressure.feature +2 -2
  10. package/features/readme.md +2 -2
  11. package/features/rpc.streams.feature +81 -0
  12. package/features/rpc.streams.shards.feature +37 -0
  13. package/features/steps/.test/context.test.js +1 -1
  14. package/features/steps/.test/events.test.js +1 -1
  15. package/features/steps/.types/context.d.ts +9 -1
  16. package/features/steps/broker.js +2 -0
  17. package/features/steps/common.js +12 -0
  18. package/features/steps/context.js +9 -2
  19. package/features/steps/rpc.js +235 -0
  20. package/lerna.json +1 -2
  21. package/package.json +3 -2
  22. package/readme.md +136 -46
  23. package/source/.io/ReplyPipe.js +158 -0
  24. package/source/.io/ReplyStream.js +139 -0
  25. package/source/.io/const.js +10 -0
  26. package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
  27. package/source/.io/index.js +5 -1
  28. package/source/attributes/failsafe.js +34 -0
  29. package/source/attributes/failsafe.test.js +133 -0
  30. package/source/attributes/index.js +13 -0
  31. package/source/attributes/lazy.js +79 -0
  32. package/source/attributes/lazy.test.js +299 -0
  33. package/source/attributes/memo.js +13 -0
  34. package/source/attributes/memo.test.js +36 -0
  35. package/source/attributes/recall.js +42 -0
  36. package/source/attributes/recall.test.js +92 -0
  37. package/source/attributes/track.js +32 -0
  38. package/source/attributes/track.test.js +133 -0
  39. package/source/channel.js +31 -18
  40. package/source/connect.js +2 -2
  41. package/source/connection.js +4 -11
  42. package/source/emitter.js +13 -0
  43. package/source/encode.js +1 -1
  44. package/source/events.js +2 -2
  45. package/source/io.js +187 -50
  46. package/source/shards/channel.js +13 -10
  47. package/source/shards/connection.js +2 -2
  48. package/test/channel.test.js +3 -1
  49. package/test/encode.test.js +2 -2
  50. package/test/encodings.js +1 -1
  51. package/test/io.diagnostics.test.js +2 -2
  52. package/test/io.request.test.js +4 -23
  53. package/test/shards/channel.test.js +1 -1
  54. package/test/shards/connection.test.js +1 -1
  55. package/types/channel.d.ts +13 -11
  56. package/types/connection.d.ts +3 -3
  57. package/types/diagnostic.d.ts +4 -3
  58. package/types/encoding.d.ts +2 -2
  59. package/types/index.d.ts +4 -4
  60. package/types/io.d.ts +33 -40
@@ -4,13 +4,13 @@ updates:
4
4
  directory: "/"
5
5
  schedule:
6
6
  interval: "weekly"
7
- target-branch: "bumps"
7
+ target-branch: "dev"
8
8
  labels:
9
9
  - "Bump"
10
10
  - package-ecosystem: "npm"
11
11
  directory: "/"
12
12
  schedule:
13
13
  interval: "daily"
14
- target-branch: "bumps"
14
+ target-branch: "dev"
15
15
  labels:
16
16
  - "Bump"
@@ -42,7 +42,7 @@ jobs:
42
42
 
43
43
  steps:
44
44
  - name: Checkout repository
45
- uses: actions/checkout@v3
45
+ uses: actions/checkout@v4
46
46
 
47
47
  # Initializes the CodeQL tools for scanning.
48
48
  - name: Initialize CodeQL
@@ -4,8 +4,8 @@ on:
4
4
  pull_request:
5
5
  types: [closed]
6
6
  branches:
7
- - main
8
- - dev
7
+ - release
8
+ - alpha
9
9
  paths:
10
10
  - 'source/**'
11
11
  - 'types/**'
@@ -27,7 +27,7 @@ jobs:
27
27
  registry-url: 'https://registry.npmjs.org'
28
28
 
29
29
  - name: Checkout
30
- uses: actions/checkout@v3
30
+ uses: actions/checkout@v4
31
31
  with:
32
32
  fetch-depth: 0
33
33
 
@@ -36,7 +36,7 @@ jobs:
36
36
 
37
37
  - name: Import GPG key
38
38
  id: gpg
39
- uses: crazy-max/ghaction-import-gpg@v5
39
+ uses: crazy-max/ghaction-import-gpg@v6
40
40
  with:
41
41
  gpg_private_key: ${{ secrets.TOABOT_GPG_PRIVATE_KEY }}
42
42
  passphrase: ${{ secrets.TOABOT_GPG_PASSPHRASE }}
@@ -45,15 +45,16 @@ jobs:
45
45
 
46
46
  - name: Bump version
47
47
  run: |
48
- if [ ${{ github.base_ref }} = main ]; then
49
- npx lerna@6.5.1 version --conventional-graduate --no-commit-hooks --no-changelog --yes --message "ci: %s" --sign-git-commit --sign-git-tag
50
- git fetch
51
- git checkout dev
52
- git merge main
53
- git push --all origin --no-verify
48
+ if [ ${{ github.base_ref }} = release ]; then
49
+ npx lerna@7.3.0 version --conventional-graduate --no-commit-hooks --no-changelog --yes --message "ci: %s" --sign-git-commit --sign-git-tag
54
50
  else
55
- npx lerna@6.5.1 version --conventional-prerelease --preid dev --dist-tag dev --no-commit-hooks --no-changelog --yes --message "ci: %s" --git-tag-command "echo %s" --sign-git-commit
51
+ npx lerna@7.3.0 version --conventional-prerelease --preid alpha --dist-tag alpha --no-commit-hooks --no-changelog --yes --message "ci: %s" --git-tag-command "echo %s" --sign-git-commit
56
52
  fi
53
+
54
+ git fetch
55
+ git checkout dev
56
+ git merge ${{ github.base_ref }}
57
+ git push --all origin --no-verify
57
58
  env:
58
59
  GITHUB_TOKEN: ${{ secrets.TOABOT_GITHUB_TOKEN }}
59
60
  GIT_AUTHOR_NAME: ${{ steps.gpg.outputs.name }}
@@ -65,10 +66,8 @@ jobs:
65
66
  env:
66
67
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
67
68
  run: |
68
- npm pkg delete scripts.prepare # husky
69
-
70
- if [ ${{ github.base_ref }} = main ]; then
69
+ if [ ${{ github.base_ref }} = release ]; then
71
70
  npm publish
72
71
  else
73
- npm publish --tag dev
72
+ npm publish --tag alpha
74
73
  fi
@@ -1,19 +1,19 @@
1
1
  # Examples
2
2
 
3
- To run examples please start RabbitMQ server with `docker compose up -d` in the package root.
3
+ To run the examples, please start RabbitMQ server with `docker compose up -d` in the package root.
4
4
 
5
5
  ## RPC
6
6
 
7
7
  Run in two terminals:
8
8
 
9
9
  ```shell
10
- $ node exampels/rpc/producer
10
+ $ node examples/rpc/producer
11
11
  ```
12
12
 
13
13
  [source](rpc/producer.js)
14
14
 
15
15
  ```shell
16
- $ node exampels/rpc/consumer
16
+ $ node examples/rpc/consumer
17
17
  ```
18
18
 
19
19
  [source](rpc/consumer.js)
@@ -41,3 +41,19 @@ $ node examples/events/producer
41
41
  `A` and `B` are consumer groups.
42
42
 
43
43
  > Try to run multiple instances with the same consumer group.
44
+
45
+ ## Reply streams
46
+
47
+ Run in two terminals:
48
+
49
+ ```shell
50
+ $ node examples/streams/producer
51
+ ```
52
+
53
+ [source](streams/producer.js)
54
+
55
+ ```shell
56
+ $ node examples/streams/consumer
57
+ ```
58
+
59
+ [source](streams/consumer.js)
@@ -0,0 +1,36 @@
1
+ import { connect } from 'comq'
2
+ import { url } from '../const.js'
3
+
4
+ const limit = Number(process.argv[2]) || Math.round(Math.random() * 8 + 2)
5
+
6
+ let io
7
+ let stream
8
+
9
+ async function run () {
10
+ io = await connect(url)
11
+
12
+ console.log('Connected')
13
+ process.on('SIGINT', exit)
14
+
15
+ await fetch()
16
+ await io.close()
17
+
18
+ console.log('Disconnected')
19
+ }
20
+
21
+ async function fetch () {
22
+ console.log(`Fetching stream with ${limit} numbers`)
23
+
24
+ stream = await io.fetch('get_numbers', { limit })
25
+
26
+ for await (const number of stream) console.log('Received:', number)
27
+
28
+ console.log('Stream finished')
29
+ }
30
+
31
+ async function exit () {
32
+ console.log()
33
+ await io.close()
34
+ }
35
+
36
+ await run()
@@ -0,0 +1,44 @@
1
+ import { connect } from 'comq'
2
+ import { url } from '../const.js'
3
+
4
+ let io
5
+
6
+ async function run () {
7
+ io = await connect(url)
8
+
9
+ console.log('Connected')
10
+
11
+ await io.reply('get_numbers', produce)
12
+
13
+ console.log('Waiting for requests...')
14
+
15
+ process.on('SIGINT', exit)
16
+ }
17
+
18
+ async function * produce ({ limit }) {
19
+ console.log('Request received with limit:', limit)
20
+
21
+ for (let i = 0; i < limit; i++) {
22
+ yield i
23
+
24
+ console.log('Yielded:', i)
25
+
26
+ await timeout()
27
+ }
28
+
29
+ console.log('Stream ended')
30
+ }
31
+
32
+ async function timeout () {
33
+ const ms = Math.round(Math.random() * 500)
34
+
35
+ await new Promise((resolve) => setTimeout(resolve, ms))
36
+ }
37
+
38
+ async function exit () {
39
+ await io.close()
40
+
41
+ console.log('\nDisconnected')
42
+ }
43
+
44
+ await run()
@@ -6,7 +6,7 @@ Feature: Back pressure handling
6
6
  Scenario: Flooding a queue
7
7
  Given function replying `flood` queue:
8
8
  """
9
- () => { return new Buffer.from('ok') }
9
+ () => { return Buffer.from('ok') }
10
10
  """
11
- When I'm sending 20kB requests to the `flood` queue at 5kHz for 0.5 seconds
11
+ When I'm sending 100kB requests to the `flood` queue at 5kHz for 0.5 seconds
12
12
  Then back pressure was applied
@@ -1,4 +1,4 @@
1
1
  # Features
2
2
 
3
- To run features please start RabbitMQ server with `docker compose up -d` in the package root, then
4
- execute `npm run test:features`.
3
+ To run features, start the RabbitMQ server by running `docker compose up -d` in the package root,
4
+ then execute `npm run features`.
@@ -0,0 +1,81 @@
1
+ Feature: Reply streams
2
+
3
+ Background:
4
+ Given an active connection to the broker
5
+ And heartbeat interval is set to 100ms
6
+ And idle timeout is set to 150ms
7
+
8
+ Scenario: Requesting a stream
9
+ Given function replying `get_numbers` queue:
10
+ """
11
+ function * ({ limit }) {
12
+ for (let i = 0; i < limit; i++) yield i
13
+ }
14
+ """
15
+ When the consumer requests a stream with the following request to the `get_numbers` queue:
16
+ """yaml
17
+ limit: 5
18
+ """
19
+ Then the consumer receives the stream:
20
+ """yaml
21
+ [0, 1, 2, 3, 4]
22
+ """
23
+
24
+ Scenario: Requesting streams concurrently
25
+ Given a number generator with 10ms increasing delay replying `get_numbers` queue
26
+ When the consumer0 requests a stream with request to the `get_numbers` queue
27
+ And the consumer0 receives the stream
28
+ And the consumer1 requests a stream with request to the `get_numbers` queue
29
+ And the consumer1 receives the stream
30
+ And after 300ms
31
+ Then the consumer0 has received the stream:
32
+ """yaml
33
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
34
+ """
35
+ And the consumer1 has received the stream:
36
+ """yaml
37
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
38
+ """
39
+
40
+ Scenario: Interrupting a stream
41
+ Given a number generator with 20ms increasing delay replying `get_numbers` queue
42
+ When the consumer requests a stream with request to the `get_numbers` queue
43
+ And the consumer interrupts the stream after 3 replies
44
+ # feedback message must be delivered
45
+ And after 10ms
46
+ Then the generator is destroyed
47
+
48
+ Scenario: Reply stream idle timeout
49
+ Given heartbeat interval is set to 300ms
50
+ And a number generator with 100ms increasing delay replying `get_numbers` queue
51
+ When the consumer requests a stream with request to the `get_numbers` queue
52
+ Then the consumer receives the stream:
53
+ """yaml
54
+ [0, 1, 3]
55
+ """
56
+ And after 500ms
57
+ Then the generator is destroyed
58
+
59
+ Scenario: Reply stream heartbeat
60
+ # delay will exceed the idle timeout on the 3rd reply
61
+ Given a number generator with 90ms increasing delay replying `get_numbers` queue
62
+ When the consumer requests a stream with request to the `get_numbers` queue
63
+ Then the consumer interrupts the stream after 5 replies
64
+ And after 200ms
65
+ Then the generator is destroyed
66
+
67
+ Scenario: Broker crashes while streaming reply
68
+ Given a number generator with 90ms increasing delay replying `get_numbers` queue
69
+ When the consumer requests a stream with request to the `get_numbers` queue
70
+ Then the consumer receives the stream
71
+ And after 300ms
72
+ Then the broker has crashed
73
+ # idle timeout
74
+ Then after 150ms
75
+ Then the consumer has received the stream:
76
+ """yaml
77
+ [0, 1, 2]
78
+ """
79
+ Then after 200ms
80
+ Then the broker is up
81
+ And the generator is destroyed
@@ -0,0 +1,37 @@
1
+ Feature: Reply streams over shards
2
+
3
+ Background:
4
+ Given an active sharded connection
5
+ And heartbeat interval is set to 100ms
6
+ And idle timeout is set to 150ms
7
+
8
+ Scenario: Requesting a stream using sharded connection
9
+ Given a generator replying `get_numbers` queue:
10
+ """
11
+ function * ({ amount }) {
12
+ for (let i = 0; i < amount; i++) yield i
13
+ }
14
+ """
15
+ When the consumer requests a stream with the following request to the `get_numbers` queue:
16
+ """yaml
17
+ amount: 5
18
+ """
19
+ Then the consumer receives the stream:
20
+ """yaml
21
+ [0, 1, 2, 3, 4]
22
+ """
23
+
24
+ Scenario: Broker crashes while fetching a stream
25
+ Given a number generator with 90ms increasing delay replying `get_numbers` queue
26
+ When the consumer requests a stream with request to the `get_numbers` queue
27
+ And the consumer receives the stream
28
+ And after 300ms
29
+ And the broker has crashed
30
+ And the broker is up
31
+ Then the consumer interrupts the stream
32
+ And the consumer has received the stream containing:
33
+ """yaml
34
+ [0, 1, 2, 3, 4, 5, 6]
35
+ """
36
+ And after 1000ms
37
+ And the generator is destroyed
@@ -59,7 +59,7 @@ describe.each([false, true])('connect (shards: %s)', (sharded) => {
59
59
 
60
60
  it.each(['open', 'close', 'flow', 'discard'])('should store %s event',
61
61
  /**
62
- * @param {comq.diagnostics.event} event
62
+ * @param {comq.diagnostics.Event} event
63
63
  */
64
64
  async (event) => {
65
65
  expect(io.diagnose).toHaveBeenCalledWith(event, expect.any(Function))
@@ -234,7 +234,7 @@ describe('Given I\'m publishing {quantity}B events to the {token} exchange at {q
234
234
 
235
235
  await timeout(100)
236
236
 
237
- const callsAmountExpected = [9, 10]
237
+ const callsAmountExpected = [8, 9, 10, 11]
238
238
 
239
239
  expect(callsAmountExpected).toContain(context.io.emit.mock.calls.length)
240
240
  expect(context.publishing).toBeDefined()
@@ -1,3 +1,4 @@
1
+ import { Readable } from 'node:stream'
1
2
  import * as _diagnostics from '../../../types/diagnostic'
2
3
  import * as _io from '../../../types/io'
3
4
  import * as _amqp from '../../../types/amqp'
@@ -15,7 +16,7 @@ declare namespace comq.features {
15
16
  eventsConsumedCount: number
16
17
  consumed?: Record<string, { payload: any, properties?: _amqp.Properties }>
17
18
  consumedCount: number
18
- events?: { [K in _diagnostics.event]?: boolean }
19
+ events?: { [K in _diagnostics.Event]?: boolean }
19
20
  exception?: Error
20
21
  consumptionPromise?: Promise<any>
21
22
  sharded: boolean
@@ -23,6 +24,13 @@ declare namespace comq.features {
23
24
  sealing: Promise<any>
24
25
  sending: any
25
26
  publishing: any
27
+ stream: Readable
28
+ streamValues: any[]
29
+ streamEnded: boolean
30
+ streams: Record<number, Readable>
31
+ streamsValues: Record<number, any[]>
32
+ streamsEnded: Record<number, boolean>
33
+ generatorDestroyed: boolean
26
34
 
27
35
  connect(user?: string, password?: string): Promise<void>
28
36
  assert(user?: string, password?: string): Promise<void>
@@ -50,6 +50,8 @@ async function healthy () {
50
50
 
51
51
  process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-0')
52
52
  } while (process.output !== 'healthy')
53
+
54
+ await timeout(500)
53
55
  }
54
56
 
55
57
  const BROKERS_AMOUNT = 2
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const { When } = require('@cucumber/cucumber')
4
+ const { timeout } = require('@toa.io/generic')
5
+
6
+ When('after {number}ms',
7
+ /**
8
+ * @param {number} delay
9
+ */
10
+ async function (delay) {
11
+ await timeout(delay)
12
+ })
@@ -22,6 +22,13 @@ class Context extends World {
22
22
  sharded
23
23
  shard
24
24
  sealing
25
+ stream
26
+ streamValues = []
27
+ streamEnded = false
28
+ streams = {}
29
+ streamsValues = {}
30
+ streamsEnded = {}
31
+ generatorDestroyed = false
25
32
 
26
33
  async connect (user, password) {
27
34
  const urls = this.#urls(user, password)
@@ -47,7 +54,7 @@ class Context extends World {
47
54
 
48
55
  /**
49
56
  * @param {string[]} urls
50
- * @param {comq.connect} [method]
57
+ * @param {comq.Connect} [method]
51
58
  * @return {Promise<void>}
52
59
  */
53
60
  async #connect (urls, method = connect) {
@@ -87,7 +94,7 @@ const SHARDS = ['localhost:5673', 'localhost:5674']
87
94
  const USER = 'developer'
88
95
  const PASSWORD = 'secret'
89
96
 
90
- /** @type {comq.diagnostics.event[]} */
97
+ /** @type {comq.diagnostics.Event[]} */
91
98
  const EVENTS = ['open', 'close', 'flow', 'discard']
92
99
 
93
100
  exports.Context = Context