comq 0.9.0 → 0.10.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/codeql.yml +1 -1
- package/.github/workflows/release.yaml +14 -15
- package/docs/reply-stream-topology-dark.jpg +0 -0
- package/docs/reply-stream-topology-light.jpg +0 -0
- package/examples/readme.md +19 -3
- package/examples/streams/consumer.js +36 -0
- package/examples/streams/producer.js +44 -0
- package/features/backpressure.feature +2 -2
- package/features/readme.md +2 -2
- package/features/rpc.streams.feature +81 -0
- package/features/rpc.streams.shards.feature +37 -0
- package/features/steps/.test/context.test.js +1 -1
- package/features/steps/.test/events.test.js +1 -1
- package/features/steps/.types/context.d.ts +9 -1
- package/features/steps/common.js +12 -0
- package/features/steps/context.js +9 -2
- package/features/steps/rpc.js +235 -0
- package/lerna.json +1 -2
- package/package.json +2 -1
- package/readme.md +140 -46
- package/source/.io/ReplyPipe.js +158 -0
- package/source/.io/ReplyStream.js +142 -0
- package/source/.io/const.js +10 -0
- package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
- package/source/.io/index.js +5 -1
- package/source/attributes/failsafe.js +34 -0
- package/source/attributes/failsafe.test.js +133 -0
- package/source/attributes/index.js +13 -0
- package/source/attributes/lazy.js +79 -0
- package/source/attributes/lazy.test.js +299 -0
- package/source/attributes/memo.js +13 -0
- package/source/attributes/memo.test.js +36 -0
- package/source/attributes/recall.js +42 -0
- package/source/attributes/recall.test.js +92 -0
- package/source/attributes/track.js +32 -0
- package/source/attributes/track.test.js +133 -0
- package/source/channel.js +31 -18
- package/source/connect.js +2 -2
- package/source/connection.js +4 -11
- package/source/emitter.js +13 -0
- package/source/encode.js +1 -1
- package/source/events.js +2 -2
- package/source/io.js +165 -47
- package/source/shards/channel.js +13 -10
- package/source/shards/connection.js +2 -2
- package/test/channel.test.js +3 -1
- package/test/encode.test.js +2 -2
- package/test/encodings.js +1 -1
- package/test/io.diagnostics.test.js +2 -2
- package/test/io.request.test.js +4 -23
- package/test/shards/channel.test.js +1 -1
- package/test/shards/connection.test.js +1 -1
- package/types/channel.d.ts +13 -11
- package/types/connection.d.ts +3 -3
- package/types/diagnostic.d.ts +4 -3
- package/types/encoding.d.ts +2 -2
- package/types/index.d.ts +4 -4
- package/types/io.d.ts +36 -40
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: "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: "
|
|
14
|
+
target-branch: "dev"
|
|
15
15
|
labels:
|
|
16
16
|
- "Bump"
|
|
@@ -4,8 +4,8 @@ on:
|
|
|
4
4
|
pull_request:
|
|
5
5
|
types: [closed]
|
|
6
6
|
branches:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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@
|
|
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@
|
|
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 }} =
|
|
49
|
-
npx lerna@
|
|
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@
|
|
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
|
-
|
|
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
|
|
72
|
+
npm publish --tag alpha
|
|
74
73
|
fi
|
|
Binary file
|
|
Binary file
|
package/examples/readme.md
CHANGED
|
@@ -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
|
|
10
|
+
$ node examples/rpc/producer
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
[source](rpc/producer.js)
|
|
14
14
|
|
|
15
15
|
```shell
|
|
16
|
-
$ node
|
|
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
|
|
9
|
+
() => { return Buffer.from('ok') }
|
|
10
10
|
"""
|
|
11
|
-
When I'm sending
|
|
11
|
+
When I'm sending 100kB requests to the `flood` queue at 5kHz for 0.5 seconds
|
|
12
12
|
Then back pressure was applied
|
package/features/readme.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# Features
|
|
2
2
|
|
|
3
|
-
To run features
|
|
4
|
-
execute `npm run
|
|
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: Fetching 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 fetches 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: Fetching streams concurrently
|
|
25
|
+
Given a number generator with 10ms increasing delay replying `get_numbers` queue
|
|
26
|
+
When the consumer0 fetches a stream with request to the `get_numbers` queue
|
|
27
|
+
And the consumer0 receives the stream
|
|
28
|
+
And the consumer1 fetches 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 fetches 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 fetches 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 fetches 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 fetches 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: Fetching 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 fetches 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 fetches 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.
|
|
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.
|
|
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>
|
|
@@ -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.
|
|
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.
|
|
97
|
+
/** @type {comq.diagnostics.Event[]} */
|
|
91
98
|
const EVENTS = ['open', 'close', 'flow', 'discard']
|
|
92
99
|
|
|
93
100
|
exports.Context = Context
|