├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ └── github-ci.yaml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .prettierrc.js
├── CHANGELOG.md
├── CONTRIBUTING.md
├── README.md
├── bin
└── build-types.sh
├── docker-compose.yml
├── examples
├── pubsub-publisher.js
├── pubsub-subscriber.js
├── receiver.js
└── sender.js
├── jest.config.js
├── package.json
├── release.config.js
├── src
├── AmqpConnectionManager.ts
├── ChannelWrapper.ts
├── helpers.ts
└── index.ts
├── test
├── .eslintrc.js
├── AmqpConnectionManagerTest.ts
├── ChannelWrapperTest.ts
├── fixtures.ts
├── importTest.ts
├── integrationTest.ts
└── tsconfig.json
├── tsconfig.cjs.json
├── tsconfig.json
└── tsconfig.types.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: ['./tsconfig.json', './test/tsconfig.json'],
5 | },
6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
7 | rules: {
8 | '@typescript-eslint/explicit-function-return-type': 'off',
9 | '@typescript-eslint/no-explicit-any': 'off',
10 | '@typescript-eslint/explicit-member-accessibility': 'off',
11 | '@typescript-eslint/no-use-before-define': 'off',
12 | // typescript compiler has better unused variable checking.
13 | '@typescript-eslint/no-unused-vars': 'off',
14 | },
15 | overrides: [
16 | {
17 | files: ['src/**/*.ts', 'src/**/*.tsx'],
18 | },
19 | {
20 | files: ['test/**/*.ts', 'test/**/*.tsx'],
21 | rules: {
22 | '@typescript-eslint/no-non-null-assertion': 'off',
23 | '@typescript-eslint/no-object-literal-type-assertion': 'off',
24 | },
25 | },
26 | ],
27 | };
28 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/github-ci.yaml:
--------------------------------------------------------------------------------
1 | name: GitHub CI
2 | on:
3 | push:
4 | branches: [master]
5 | tags:
6 | - '*'
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [14.x, 16.x, 'lts/*']
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Use Node.js ${{ matrix.node-version }}
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 | - run: docker-compose up -d
22 | - run: npm install
23 | - run: npm test
24 | # - name: Coveralls
25 | # uses: coverallsapp/github-action@master
26 | # with:
27 | # github-token: ${{ SECRETS.GITHUB_TOKEN }}
28 | # flag-name: run-${{ matrix.node-version }}
29 | # parallel: true
30 | # coveralls:
31 | # needs: test
32 | # runs-on: ubuntu-latest
33 | # steps:
34 | # - name: Coveralls Finished
35 | # uses: coverallsapp/github-action@master
36 | # with:
37 | # github-token: ${{ SECRETS.GITHUB_TOKEN }}
38 | # parallel-finished: true
39 | release:
40 | # Only release on push to master
41 | if: github.event_name == 'push' && github.ref == 'refs/heads/master'
42 | runs-on: ubuntu-latest
43 | # Waits for test jobs for each Node.js version to complete
44 | needs: [test]
45 | steps:
46 | - uses: actions/checkout@v2
47 | - name: Use Node.js
48 | uses: actions/setup-node@v2
49 | with:
50 | node-version: 'lts/*'
51 | - run: npm install
52 | - name: semantic-release
53 | run: npm run semantic-release
54 | env:
55 | GH_TOKEN: ${{ secrets.DEPLOY_GH_TOKEN }}
56 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /lib
3 | /npm-debug.log
4 | /coverage
5 | /package-lock.json
6 | /.nyc_output
7 | /.vscode
8 | /dist
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx --no-install pretty-quick --staged
2 | npx --no-install lint-staged
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'es5',
3 | printWidth: 100,
4 | tabWidth: 4,
5 | semi: true,
6 | singleQuote: true,
7 | overrides: [
8 | {
9 | files: '*.md',
10 | options: {
11 | tabWidth: 2,
12 | },
13 | },
14 | {
15 | files: '*.yml',
16 | options: {
17 | tabWidth: 2,
18 | },
19 | },
20 | {
21 | files: '*.yaml',
22 | options: {
23 | tabWidth: 2,
24 | },
25 | },
26 | ],
27 | };
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [4.1.14](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.13...v4.1.14) (2023-07-27)
2 |
3 | ### Bug Fixes
4 |
5 | - added type build step ([0cc9859](https://github.com/jwalton/node-amqp-connection-manager/commit/0cc9859c47bd54f1d449568f665426755064cba4))
6 | - export types in separate directory ([5d6cdbf](https://github.com/jwalton/node-amqp-connection-manager/commit/5d6cdbfc9340c2b8a2b93d40277656ccaa937cae))
7 |
8 | ## [4.1.13](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.12...v4.1.13) (2023-05-02)
9 |
10 | ### Bug Fixes
11 |
12 | - **types:** move `types` condition to the front ([a1eb206](https://github.com/jwalton/node-amqp-connection-manager/commit/a1eb206a86aff49164c766575ddb7717c0847c44))
13 |
14 | ## [4.1.12](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.11...v4.1.12) (2023-04-03)
15 |
16 | ### Bug Fixes
17 |
18 | - **types:** Export types for ESM. ([7d3755a](https://github.com/jwalton/node-amqp-connection-manager/commit/7d3755a0a9bdef6a55ee2bd61d450071727beb52)), closes [#329](https://github.com/jwalton/node-amqp-connection-manager/issues/329)
19 |
20 | ## [4.1.11](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.10...v4.1.11) (2023-02-24)
21 |
22 | ### Bug Fixes
23 |
24 | - Add unbindQueue to ChannelWrapper. ([55ce8d3](https://github.com/jwalton/node-amqp-connection-manager/commit/55ce8d37d64fc946e95f238ea3ee3e034e6dd456))
25 |
26 | ## [4.1.10](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.9...v4.1.10) (2022-12-31)
27 |
28 | ### Bug Fixes
29 |
30 | - exporting ChannelWrapper as a type without it getting emitted as metadata ([a6f7b5c](https://github.com/jwalton/node-amqp-connection-manager/commit/a6f7b5c7332b41229f90717f5c4650a04fe1dbba))
31 |
32 | ## [4.1.9](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.8...v4.1.9) (2022-10-24)
33 |
34 | ### Bug Fixes
35 |
36 | - Fail immediately for a bad password on latest amqplib. ([412ed92](https://github.com/jwalton/node-amqp-connection-manager/commit/412ed921be20494e87f5f6b5c9a65ad2f207d304))
37 |
38 | ## [4.1.8](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.7...v4.1.8) (2022-10-24)
39 |
40 | ### Bug Fixes
41 |
42 | - error thrown when queue deleted in amqplib 0.10.0 ([60700ee](https://github.com/jwalton/node-amqp-connection-manager/commit/60700eebcac6f1ce985a34da59f216481473780a)), closes [#301](https://github.com/jwalton/node-amqp-connection-manager/issues/301)
43 |
44 | ## [4.1.7](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.6...v4.1.7) (2022-09-30)
45 |
46 | ### Bug Fixes
47 |
48 | - consumer registered twice during setup ([1ca216a](https://github.com/jwalton/node-amqp-connection-manager/commit/1ca216a47c2abdbb1f4a1e04b9032cb03db17aa2)), closes [#297](https://github.com/jwalton/node-amqp-connection-manager/issues/297)
49 |
50 | ## [4.1.6](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.5...v4.1.6) (2022-08-11)
51 |
52 | ### Bug Fixes
53 |
54 | - Upgrade promise-breaker to 6.0.0 to fix typescript imports. ([c9aff08](https://github.com/jwalton/node-amqp-connection-manager/commit/c9aff0893336dab3e440825f56c3e94aa39d9ecc)), closes [#234](https://github.com/jwalton/node-amqp-connection-manager/issues/234)
55 |
56 | ## [4.1.5](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.4...v4.1.5) (2022-08-09)
57 |
58 | ### Reverts
59 |
60 | - Revert "fix: import of promise breaker" ([aaeae1e](https://github.com/jwalton/node-amqp-connection-manager/commit/aaeae1e3e29ce5ef9598e6e8be1f377d9559ee2e))
61 |
62 | ## [4.1.4](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.3...v4.1.4) (2022-08-05)
63 |
64 | ### Bug Fixes
65 |
66 | - import of promise breaker ([d873885](https://github.com/jwalton/node-amqp-connection-manager/commit/d87388550d85e8326f6d91b6f86819809e1401ae)), closes [#234](https://github.com/jwalton/node-amqp-connection-manager/issues/234)
67 |
68 | ## [4.1.3](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.2...v4.1.3) (2022-05-04)
69 |
70 | ### Bug Fixes
71 |
72 | - accept 0 for heartbeatIntervalInSeconds ([208af68](https://github.com/jwalton/node-amqp-connection-manager/commit/208af6875bacda01b5a75be52f631bd71b4eafcd))
73 |
74 | ## [4.1.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.1...v4.1.2) (2022-04-13)
75 |
76 | ### Bug Fixes
77 |
78 | - **types:** Export PublishOptions type. ([6d20252](https://github.com/jwalton/node-amqp-connection-manager/commit/6d2025204d3adc050f916c2c116c9ac8db36114c))
79 |
80 | ## [4.1.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.1.0...v4.1.1) (2022-02-05)
81 |
82 | ### Bug Fixes
83 |
84 | - process unable to exit after connect ([8d572b1](https://github.com/jwalton/node-amqp-connection-manager/commit/8d572b1899f3739bb887104dd992aff6722f137a))
85 |
86 | # [4.1.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.0.1...v4.1.0) (2022-02-01)
87 |
88 | ### Features
89 |
90 | - cancel specific consumer ([5f3b2eb](https://github.com/jwalton/node-amqp-connection-manager/commit/5f3b2eb1ab20d2fc63054192632a80d78a7934a2))
91 |
92 | ## [4.0.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v4.0.0...v4.0.1) (2022-01-21)
93 |
94 | ### Bug Fixes
95 |
96 | - accept type of amqplib.credentials.external() ([1db3b2d](https://github.com/jwalton/node-amqp-connection-manager/commit/1db3b2d5339387124c9b7b43192b7d93db7b1702))
97 |
98 | # [4.0.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.9.0...v4.0.0) (2022-01-07)
99 |
100 | ### Bug Fixes
101 |
102 | - Emit `connectFailed` on connection failure. ([0f05987](https://github.com/jwalton/node-amqp-connection-manager/commit/0f05987af16db25954fb83de4e0bed05e71121cf)), closes [#222](https://github.com/jwalton/node-amqp-connection-manager/issues/222)
103 |
104 | ### Continuous Integration
105 |
106 | - Stop testing on node 10 and 12. ([5da9cb0](https://github.com/jwalton/node-amqp-connection-manager/commit/5da9cb034ec1f311677fd0db3931176ee2797dc2))
107 |
108 | ### BREAKING CHANGES
109 |
110 | - No longer running unit tests on node 10 and 12, although this package may continue to work on these.
111 | - We will no longer emit a `disconnect` event on an
112 | initial connection failure - instead we now emit `connectFailed` on each
113 | connection failure, and only emit `disconnect` when we transition from
114 | connected to disconnected.
115 |
116 | # [3.9.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.8.1...v3.9.0) (2022-01-04)
117 |
118 | ### Features
119 |
120 | - proxying every exchange function of amqplib ([bca347c](https://github.com/jwalton/node-amqp-connection-manager/commit/bca347c437ec116249a671b4b903d7668e097cf8))
121 |
122 | ## [3.8.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.8.0...v3.8.1) (2021-12-29)
123 |
124 | ### Bug Fixes
125 |
126 | - batch sending stops ([6a5b589](https://github.com/jwalton/node-amqp-connection-manager/commit/6a5b5890de3e2d543e27f4c078d27dc32b8b23d9)), closes [#196](https://github.com/jwalton/node-amqp-connection-manager/issues/196)
127 |
128 | # [3.8.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.7.0...v3.8.0) (2021-12-29)
129 |
130 | ### Features
131 |
132 | - plain channel ([328d31d](https://github.com/jwalton/node-amqp-connection-manager/commit/328d31d758ad32df49c8fe03d89b1f6d06a9465b))
133 |
134 | # [3.7.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.6.0...v3.7.0) (2021-09-21)
135 |
136 | ### Bug Fixes
137 |
138 | - **AmqpConnectionManager:** IAmqpConnectionManager interface definition ([dedec7e](https://github.com/jwalton/node-amqp-connection-manager/commit/dedec7e24f12395e4ff2424d5f4026a5551ba064))
139 |
140 | ### Features
141 |
142 | - add default publish timeout ([6826be2](https://github.com/jwalton/node-amqp-connection-manager/commit/6826be26ab6ed786832251ee06ff7bfc303d775c))
143 | - expose AmqpConnectionManagerClass ([835a81f](https://github.com/jwalton/node-amqp-connection-manager/commit/835a81f0c953d5ab2a01611d277478d5b78aa8b0))
144 | - timeout option for publish ([dee380d](https://github.com/jwalton/node-amqp-connection-manager/commit/dee380d7ed70cc801166e8859fff5e66bd6b9ece))
145 |
146 | # [3.6.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.5.2...v3.6.0) (2021-08-27)
147 |
148 | ### Features
149 |
150 | - reconnect and cancelAll consumers ([fb0c00b](https://github.com/jwalton/node-amqp-connection-manager/commit/fb0c00becc224ffedd28e810cbb314187d21efdb))
151 |
152 | ## [3.5.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.5.1...v3.5.2) (2021-08-26)
153 |
154 | ### Bug Fixes
155 |
156 | - Fix handling of resending messages during a disconnect. ([e1457a5](https://github.com/jwalton/node-amqp-connection-manager/commit/e1457a598c6ecffca9c864036f1875f546ad5017)), closes [#152](https://github.com/jwalton/node-amqp-connection-manager/issues/152)
157 |
158 | ### Performance Improvements
159 |
160 | - Send messages to underlying channel in synchronous batches. ([b866ef2](https://github.com/jwalton/node-amqp-connection-manager/commit/b866ef25ebe97c1cf4fe421835291584cb738f41))
161 |
162 | ## [3.5.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.5.0...v3.5.1) (2021-08-26)
163 |
164 | ### Bug Fixes
165 |
166 | - **types:** Make private things private. ([8b1338b](https://github.com/jwalton/node-amqp-connection-manager/commit/8b1338ba2f46267b4aedcbeeaccfdc6cb24680ec))
167 |
168 | # [3.5.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.5...v3.5.0) (2021-08-26)
169 |
170 | ### Features
171 |
172 | - manual reconnect ([798b45f](https://github.com/jwalton/node-amqp-connection-manager/commit/798b45f52c437f35a0f89b431b872354a7a3eb0e))
173 |
174 | ## [3.4.5](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.4...v3.4.5) (2021-08-26)
175 |
176 | ### Performance Improvements
177 |
178 | - resolve sent messages immediately ([2349da2](https://github.com/jwalton/node-amqp-connection-manager/commit/2349da2db8d34934c6d0225983b6411509730560))
179 |
180 | ## [3.4.4](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.3...v3.4.4) (2021-08-26)
181 |
182 | ### Bug Fixes
183 |
184 | - **types:** Allow passing object to `connect()` in addition to strings. ([516fd9f](https://github.com/jwalton/node-amqp-connection-manager/commit/516fd9fa19a12ce6aa585f97503dfb4fa336352f))
185 |
186 | ## [3.4.3](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.2...v3.4.3) (2021-08-25)
187 |
188 | ### Bug Fixes
189 |
190 | - **types:** 'options' should be optional in `connect()`. ([4619149](https://github.com/jwalton/node-amqp-connection-manager/commit/4619149e702418b9e4bba6e135c675754589a8ed))
191 | - Fix bluebird warning. ([cb2f124](https://github.com/jwalton/node-amqp-connection-manager/commit/cb2f124e10d193f6c9a3cb255636e112be2c4e53)), closes [#171](https://github.com/jwalton/node-amqp-connection-manager/issues/171)
192 |
193 | ## [3.4.3](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.2...v3.4.3) (2021-08-25)
194 |
195 | ### Bug Fixes
196 |
197 | - Fix bluebird warning. ([cb2f124](https://github.com/jwalton/node-amqp-connection-manager/commit/cb2f124e10d193f6c9a3cb255636e112be2c4e53)), closes [#171](https://github.com/jwalton/node-amqp-connection-manager/issues/171)
198 |
199 | ## [3.4.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.1...v3.4.2) (2021-08-25)
200 |
201 | ### Bug Fixes
202 |
203 | - **types:** Minor type fixes. ([6865613](https://github.com/jwalton/node-amqp-connection-manager/commit/68656134a13786af2b751527e5d03eff07dd72b9))
204 |
205 | ## [3.4.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.4.0...v3.4.1) (2021-08-25)
206 |
207 | ### Bug Fixes
208 |
209 | - Only send disconnect event on first error. ([efde3b9](https://github.com/jwalton/node-amqp-connection-manager/commit/efde3b919252f031a3edf2a06d7cfe280c06044f)), closes [#145](https://github.com/jwalton/node-amqp-connection-manager/issues/145)
210 |
211 | # [3.4.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.3.0...v3.4.0) (2021-08-25)
212 |
213 | ### Features
214 |
215 | - Convert to typescript, add module exports. ([5f442b1](https://github.com/jwalton/node-amqp-connection-manager/commit/5f442b139dfe15468fef32022b87f409cd781a78))
216 |
217 | # [3.3.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.2.4...v3.3.0) (2021-08-24)
218 |
219 | ### Bug Fixes
220 |
221 | - emit setup errors not caused by closed channel ([7c5fe10](https://github.com/jwalton/node-amqp-connection-manager/commit/7c5fe104c5333086a8b06bc28f451b4f22cc489d)), closes [#95](https://github.com/jwalton/node-amqp-connection-manager/issues/95)
222 | - setup on channel/connection closing/closed ([b21bd01](https://github.com/jwalton/node-amqp-connection-manager/commit/b21bd0173dc60712cedfd398161e52b6f621bf2a))
223 |
224 | ### Features
225 |
226 | - immediately reconnect on amqplib connect timeout ([ad06108](https://github.com/jwalton/node-amqp-connection-manager/commit/ad0610878f0aba27cc5078a6d1e61420a77b7965))
227 |
228 | ## [3.2.4](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.2.3...v3.2.4) (2021-08-23)
229 |
230 | ### Bug Fixes
231 |
232 | - connection close not awaited ([8955fe7](https://github.com/jwalton/node-amqp-connection-manager/commit/8955fe7ee8f52505b629ee091f3658dfeb425c37))
233 |
234 | ## [3.2.3](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.2.2...v3.2.3) (2021-08-21)
235 |
236 | ### Bug Fixes
237 |
238 | - fixed issue with publish ignoring 'drain' event ([e195d9b](https://github.com/jwalton/node-amqp-connection-manager/commit/e195d9bc29907de49eec5e09aea3bba0b894d965)), closes [#129](https://github.com/jwalton/node-amqp-connection-manager/issues/129)
239 |
240 | ## [3.2.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.2.1...v3.2.2) (2021-02-09)
241 |
242 | ### Bug Fixes
243 |
244 | - When messages are acked/nacked, make sure we remove the correct message from the sent messages queue. ([c662026](https://github.com/jwalton/node-amqp-connection-manager/commit/c662026bc287e684a0f43ce2de7a44b80a88e8ff)), closes [#142](https://github.com/jwalton/node-amqp-connection-manager/issues/142)
245 |
246 | ## [3.2.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.2.0...v3.2.1) (2020-09-12)
247 |
248 | ### Bug Fixes
249 |
250 | - Push never resolves if error occured (courtesy @SSANSH). ([48a78f8](https://github.com/jwalton/node-amqp-connection-manager/commit/48a78f8de5d39002035b37f27fc3e0ce5015490c))
251 | - **package:** resolve hanging retry connection timeout by introducing cancelable timeout ([e37dd1a](https://github.com/jwalton/node-amqp-connection-manager/commit/e37dd1a4e423012910d31ae8bcebf781cac6f3b5))
252 |
253 | # [3.2.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.1.1...v3.2.0) (2020-01-20)
254 |
255 | ### Features
256 |
257 | - add bindQueue and assertExchange on ChannelWrapper ([879e522](https://github.com/jwalton/node-amqp-connection-manager/commit/879e522))
258 |
259 | ## [3.1.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.1.0...v3.1.1) (2020-01-06)
260 |
261 | ### Bug Fixes
262 |
263 | - typo ([6055b02](https://github.com/jwalton/node-amqp-connection-manager/commit/6055b02))
264 |
265 | # [3.1.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v3.0.0...v3.1.0) (2019-12-06)
266 |
267 | ### Features
268 |
269 | - Allow using URL object to connect, same format as amqplib accepts. ([f046680](https://github.com/jwalton/node-amqp-connection-manager/commit/f046680))
270 |
271 | # [3.0.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.3.3...v3.0.0) (2019-07-04)
272 |
273 | ### Continuous Integration
274 |
275 | - Stop running tests for node 6 and node 8. ([164b882](https://github.com/jwalton/node-amqp-connection-manager/commit/164b882))
276 |
277 | ### BREAKING CHANGES
278 |
279 | - Officially drop support for node 6 and node 8 (although they will probably still
280 | work).
281 |
282 | ## [2.3.3](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.3.2...v2.3.3) (2019-06-25)
283 |
284 | ### Bug Fixes
285 |
286 | - **package:** update promise-breaker to version 5.0.0 ([ed91042](https://github.com/jwalton/node-amqp-connection-manager/commit/ed91042))
287 |
288 | ## [2.3.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.3.1...v2.3.2) (2019-05-21)
289 |
290 | ### Bug Fixes
291 |
292 | - Null delta to get semantic-release to pick up [#65](https://github.com/jwalton/node-amqp-connection-manager/issues/65). Fix [#84](https://github.com/jwalton/node-amqp-connection-manager/issues/84). ([9737135](https://github.com/jwalton/node-amqp-connection-manager/commit/9737135))
293 |
294 | ## [2.3.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.3.0...v2.3.1) (2019-04-01)
295 |
296 | ### Bug Fixes
297 |
298 | - prevent too many connection attempts on error ([2760ce5](https://github.com/jwalton/node-amqp-connection-manager/commit/2760ce5)), closes [#77](https://github.com/jwalton/node-amqp-connection-manager/issues/77)
299 |
300 | # [2.3.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.2.0...v2.3.0) (2018-11-20)
301 |
302 | ### Features
303 |
304 | - Add ChannelWrapper.ackAll() and ChannelWrapper.nackAll(). ([0246695](https://github.com/jwalton/node-amqp-connection-manager/commit/0246695)), closes [#60](https://github.com/jwalton/node-amqp-connection-manager/issues/60)
305 |
306 | # [2.2.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.1.2...v2.2.0) (2018-09-25)
307 |
308 | ### Features
309 |
310 | - Set 'this' to be the channel wrapper in the setup function. ([551200f](https://github.com/jwalton/node-amqp-connection-manager/commit/551200f))
311 |
312 | ## [2.1.2](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.1.1...v2.1.2) (2018-09-13)
313 |
314 | ### Bug Fixes
315 |
316 | - Export a default object from root module. ([78893c9](https://github.com/jwalton/node-amqp-connection-manager/commit/78893c9)), closes [#51](https://github.com/jwalton/node-amqp-connection-manager/issues/51)
317 |
318 | ## [2.1.1](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.1.0...v2.1.1) (2018-09-05)
319 |
320 | ### Bug Fixes
321 |
322 | - Remove reconnection listener when closing the connection manager. ([eeb6e2b](https://github.com/jwalton/node-amqp-connection-manager/commit/eeb6e2b))
323 |
324 | # [2.1.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v2.0.0...v2.1.0) (2018-08-09)
325 |
326 | ### Features
327 |
328 | - Support for per URL connection options ([ec2d484](https://github.com/jwalton/node-amqp-connection-manager/commit/ec2d484)), closes [#29](https://github.com/jwalton/node-amqp-connection-manager/issues/29) [#34](https://github.com/jwalton/node-amqp-connection-manager/issues/34) [#44](https://github.com/jwalton/node-amqp-connection-manager/issues/44)
329 |
330 |
331 |
332 | # [2.0.0](https://github.com/jwalton/node-amqp-connection-manager/compare/v1.4.2...v2.0.0) (2018-05-05)
333 |
334 | ### Code Refactoring
335 |
336 | - Rewrite all source in javascript. ([377d01d](https://github.com/jwalton/node-amqp-connection-manager/commit/377d01d))
337 |
338 | ### BREAKING CHANGES
339 |
340 | - Officially dropping support for node v4.x.x.
341 |
342 | # 1.4.0
343 |
344 | - Add 'blocked' and 'unblocked' events (#25).
345 |
346 | # 1.3.7
347 |
348 | - Fix bug where we would stop sending messages if remote gracefully closes connection.
349 |
350 | # 1.3.6
351 |
352 | - Fix bug where ChannelWrapper would expect setup function to return a Promise
353 | and not accept a callback if channel was already connected.
354 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This project uses [semantic-release](https://github.com/semantic-release/semantic-release)
4 | so commit messages should follow [Angular commit message conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines).
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # amqp-connection-manager
2 |
3 | [](https://npmjs.org/package/amqp-connection-manager)
4 | 
5 | [](https://github.com/semantic-release/semantic-release)
6 |
7 | Connection management for amqplib. This is a wrapper around [amqplib](http://www.squaremobius.net/amqp.node/) which provides automatic reconnects.
8 |
9 | ## Features
10 |
11 | - Automatically reconnect when your [amqplib](http://www.squaremobius.net/amqp.node/) broker dies in a fire.
12 | - Round-robin connections between multiple brokers in a cluster.
13 | - If messages are sent while the broker is unavailable, queues messages in memory until we reconnect.
14 | - Supports both promises and callbacks (using [promise-breaker](https://github.com/jwalton/node-promise-breaker))
15 | - Very un-opinionated library - a thin wrapper around [amqplib](http://www.squaremobius.net/amqp.node/).
16 |
17 | ## Installation
18 |
19 | ```sh
20 | npm install --save amqplib amqp-connection-manager
21 | ```
22 |
23 | ## Basics
24 |
25 | The basic idea here is that, usually, when you create a new channel, you do some
26 | setup work at the beginning (like asserting that various queues or exchanges
27 | exist, or binding to queues), and then you send and receive messages and you
28 | never touch that stuff again.
29 |
30 | amqp-connection-manager will reconnect to a new broker whenever the broker it is
31 | currently connected to dies. When you ask amqp-connection-manager for a
32 | channel, you specify one or more `setup` functions to run; the setup functions
33 | will be run every time amqp-connection-manager reconnects, to make sure your
34 | channel and broker are in a sane state.
35 |
36 | Before we get into an example, note this example is written using Promises,
37 | however much like amqplib, any function which returns a Promise will also accept
38 | a callback as an optional parameter.
39 |
40 | Here's the example:
41 |
42 | ```js
43 | var amqp = require('amqp-connection-manager');
44 |
45 | // Create a new connection manager
46 | var connection = amqp.connect(['amqp://localhost']);
47 |
48 | // Ask the connection manager for a ChannelWrapper. Specify a setup function to
49 | // run every time we reconnect to the broker.
50 | var channelWrapper = connection.createChannel({
51 | json: true,
52 | setup: function (channel) {
53 | // `channel` here is a regular amqplib `ConfirmChannel`.
54 | // Note that `this` here is the channelWrapper instance.
55 | return channel.assertQueue('rxQueueName', { durable: true });
56 | },
57 | });
58 |
59 | // Send some messages to the queue. If we're not currently connected, these will be queued up in memory
60 | // until we connect. Note that `sendToQueue()` and `publish()` return a Promise which is fulfilled or rejected
61 | // when the message is actually sent (or not sent.)
62 | channelWrapper
63 | .sendToQueue('rxQueueName', { hello: 'world' })
64 | .then(function () {
65 | return console.log('Message was sent! Hooray!');
66 | })
67 | .catch(function (err) {
68 | return console.log('Message was rejected... Boo!');
69 | });
70 | ```
71 |
72 | Sometimes it's handy to modify a channel at run time. For example, suppose you
73 | have a channel that's listening to one kind of message, and you decide you now
74 | also want to listen to some other kind of message. This can be done by adding a
75 | new setup function to an existing ChannelWrapper:
76 |
77 | ```js
78 | channelWrapper.addSetup(function (channel) {
79 | return Promise.all([
80 | channel.assertQueue('my-queue', { exclusive: true, autoDelete: true }),
81 | channel.bindQueue('my-queue', 'my-exchange', 'create'),
82 | channel.consume('my-queue', handleMessage),
83 | ]);
84 | });
85 | ```
86 |
87 | `addSetup()` returns a Promise which resolves when the setup function is
88 | finished (or immediately, if the underlying connection is not currently
89 | connected to a broker.) There is also a `removeSetup(setup, teardown)` which
90 | will run `teardown(channel)` if the channel is currently connected to a broker
91 | (and will not run `teardown` at all otherwise.) Note that `setup` and `teardown`
92 | _must_ either accept a callback or return a Promise.
93 |
94 | See a complete example in the [examples](./examples) folder.
95 |
96 | ## API
97 |
98 | ### connect(urls, options)
99 |
100 | Creates a new AmqpConnectionManager, which will connect to one of the URLs provided in `urls`. If a broker is
101 | unreachable or dies, then AmqpConnectionManager will try the next available broker, round-robin.
102 |
103 | Options:
104 |
105 | - `options.heartbeatIntervalInSeconds` - Interval to send heartbeats to broker. Defaults to 5 seconds.
106 | - `options.reconnectTimeInSeconds` - The time to wait before trying to reconnect. If not specified,
107 | defaults to `heartbeatIntervalInSeconds`.
108 | - `options.findServers(callback)` is a function which returns one or more servers to connect to. This should
109 | return either a single URL or an array of URLs. This is handy when you're using a service discovery mechanism.
110 | such as Consul or etcd. Instead of taking a `callback`, this can also return a Promise. Note that if this
111 | is supplied, then `urls` is ignored.
112 | - `options.connectionOptions` is passed as options to the amqplib connect method.
113 |
114 | ### AmqpConnectionManager events
115 |
116 | - `connect({connection, url})` - Emitted whenever we successfully connect to a broker.
117 | - `connectFailed({err, url})` - Emitted whenever we attempt to connect to a broker, but fail.
118 | - `disconnect({err})` - Emitted whenever we disconnect from a broker.
119 | - `blocked({reason})` - Emitted whenever a connection is blocked by a broker
120 | - `unblocked` - Emitted whenever a connection is unblocked by a broker
121 |
122 | ### AmqpConnectionManager#createChannel(options)
123 |
124 | Create a new ChannelWrapper. This is a proxy for the actual channel (which may or may not exist at any moment,
125 | depending on whether or not we are currently connected.)
126 |
127 | Options:
128 |
129 | - `options.name` - Name for this channel. Used for debugging.
130 | - `options.setup(channel, [cb])` - A function to call whenever we reconnect to the
131 | broker (and therefore create a new underlying channel.) This function should
132 | either accept a callback, or return a Promise. See `addSetup` below.
133 | Note that `this` inside the setup function will the returned ChannelWrapper.
134 | The ChannelWrapper has a special `context` member you can use to store
135 | arbitrary data in.
136 | - `options.json` - if true, then ChannelWrapper assumes all messages passed to `publish()` and `sendToQueue()`
137 | are plain JSON objects. These will be encoded automatically before being sent.
138 | - `options.confirm` - if true (default), the created channel will be a ConfirmChannel
139 | - `options.publishTimeout` - a default timeout for messages published to this channel.
140 |
141 | ### AmqpConnectionManager#isConnected()
142 |
143 | Returns true if the AmqpConnectionManager is connected to a broker, false otherwise.
144 |
145 | ### AmqpConnectionManager#close()
146 |
147 | Close this AmqpConnectionManager and free all associated resources.
148 |
149 | ### ChannelWrapper events
150 |
151 | - `connect` - emitted every time this channel connects or reconnects.
152 | - `error(err, {name})` - emitted if an error occurs setting up the channel.
153 | - `close` - emitted when this channel closes via a call to `close()`
154 |
155 | ### ChannelWrapper#addSetup(setup)
156 |
157 | Adds a new 'setup handler'.
158 |
159 | `setup(channel, [cb])` is a function to call when a new underlying channel is created - handy for asserting
160 | exchanges and queues exists, and whatnot. The `channel` object here is a ConfirmChannel from amqplib.
161 | The `setup` function should return a Promise (or optionally take a callback) - no messages will be sent until
162 | this Promise resolves.
163 |
164 | If there is a connection, `setup()` will be run immediately, and the addSetup Promise/callback won't resolve
165 | until `setup` is complete. Note that in this case, if the setup throws an error, no 'error' event will
166 | be emitted, since you can just handle the error here (although the `setup` will still be added for future
167 | reconnects, even if it throws an error.)
168 |
169 | Setup functions should, ideally, not throw errors, but if they do then the ChannelWrapper will emit an 'error'
170 | event.
171 |
172 | ### ChannelWrapper#removeSetup(setup, teardown)
173 |
174 | Removes a setup handler. If the channel is currently connected, will call `teardown(channel)`, passing in the
175 | underlying amqplib ConfirmChannel. `teardown` should either take a callback or return a Promise.
176 |
177 | ### ChannelWrapper#publish and ChannelWrapper#sendToQueue
178 |
179 | These work exactly like their counterparts in amqplib's Channel, except that they return a Promise (or accept a
180 | callback) which resolves when the message is confirmed to have been delivered to the broker. The promise rejects if
181 | either the broker refuses the message, or if `close()` is called on the ChannelWrapper before the message can be
182 | delivered.
183 |
184 | Both of these functions take an additional option when passing options:
185 |
186 | - `timeout` - If specified, if a messages is not acked by the amqp broker within the specified number of milliseconds,
187 | the message will be rejected. Note that the message _may_ still end up getting delivered after the timeout, as we
188 | have no way to cancel the in-flight request.
189 |
190 | ### ChannelWrapper#ack and ChannelWrapper#nack
191 |
192 | These are just aliases for calling `ack()` and `nack()` on the underlying channel. They do nothing if the underlying
193 | channel is not connected.
194 |
195 | ### ChannelWrapper#queueLength()
196 |
197 | Returns a count of messages currently waiting to be sent to the underlying channel.
198 |
199 | ### ChannelWrapper#close()
200 |
201 | Close a channel, clean up resources associated with it.
202 |
--------------------------------------------------------------------------------
/bin/build-types.sh:
--------------------------------------------------------------------------------
1 | cat >dist/cjs/package.json <dist/esm/package.json < console.log('Connected!'));
9 | connection.on('disconnect', err => console.log('Disconnected.', err.stack));
10 |
11 | // Create a channel wrapper
12 | const channelWrapper = connection.createChannel({
13 | json: true,
14 | setup: channel => channel.assertExchange(EXCHANGE_NAME, 'topic')
15 | });
16 |
17 | // Send messages until someone hits CTRL-C or something goes wrong...
18 | function sendMessage() {
19 | channelWrapper.publish(EXCHANGE_NAME, "test", {time: Date.now()}, { contentType: 'application/json', persistent: true })
20 | .then(function() {
21 | console.log("Message sent");
22 | return wait(1000);
23 | })
24 | .then(() => sendMessage())
25 | .catch(err => {
26 | console.log("Message was rejected:", err.stack);
27 | channelWrapper.close();
28 | connection.close();
29 | });
30 | };
31 |
32 | console.log("Sending messages...");
33 | sendMessage();
34 |
--------------------------------------------------------------------------------
/examples/pubsub-subscriber.js:
--------------------------------------------------------------------------------
1 | const amqp = require('..');
2 |
3 | const QUEUE_NAME = 'amqp-connection-manager-sample2'
4 | const EXCHANGE_NAME = 'amqp-connection-manager-sample2-ex';
5 |
6 | // Handle an incomming message.
7 | const onMessage = data => {
8 | var message = JSON.parse(data.content.toString());
9 | console.log("subscriber: got message", message);
10 | channelWrapper.ack(data);
11 | }
12 |
13 | // Create a connetion manager
14 | const connection = amqp.connect(['amqp://localhost']);
15 | connection.on('connect', () => console.log('Connected!'));
16 | connection.on('disconnect', err => console.log('Disconnected.', err.stack));
17 |
18 | // Set up a channel listening for messages in the queue.
19 | var channelWrapper = connection.createChannel({
20 | setup: channel =>
21 | // `channel` here is a regular amqplib `ConfirmChannel`.
22 | return Promise.all([
23 | channel.assertQueue(QUEUE_NAME, { exclusive: true, autoDelete: true }),
24 | channel.assertExchange(EXCHANGE_NAME, 'topic'),
25 | channel.prefetch(1),
26 | channel.bindQueue(QUEUE_NAME, EXCHANGE_NAME, '#'),
27 | channel.consume(QUEUE_NAME, onMessage)
28 | ])
29 | });
30 |
31 | channelWrapper.waitForConnect()
32 | .then(function() {
33 | console.log("Listening for messages");
34 | });
35 |
--------------------------------------------------------------------------------
/examples/receiver.js:
--------------------------------------------------------------------------------
1 | var amqp = require('..');
2 |
3 | var QUEUE_NAME = 'amqp-connection-manager-sample1'
4 |
5 | // Handle an incomming message.
6 | var onMessage = function(data) {
7 | var message = JSON.parse(data.content.toString());
8 | console.log("receiver: got message", message);
9 | channelWrapper.ack(data);
10 | }
11 |
12 | // Create a connetion manager
13 | var connection = amqp.connect(['amqp://localhost']);
14 | connection.on('connect', function() {
15 | console.log('Connected!');
16 | });
17 | connection.on('disconnect', function(err) {
18 | console.log('Disconnected.', err.stack);
19 | });
20 |
21 | // Set up a channel listening for messages in the queue.
22 | var channelWrapper = connection.createChannel({
23 | setup: function(channel) {
24 | // `channel` here is a regular amqplib `ConfirmChannel`.
25 | return Promise.all([
26 | channel.assertQueue(QUEUE_NAME, {durable: true}),
27 | channel.prefetch(1),
28 | channel.consume(QUEUE_NAME, onMessage)
29 | ]);
30 | }
31 | });
32 |
33 | channelWrapper.waitForConnect()
34 | .then(function() {
35 | console.log("Listening for messages");
36 | });
37 |
--------------------------------------------------------------------------------
/examples/sender.js:
--------------------------------------------------------------------------------
1 | var amqp = require('..');
2 | var wait = require('../lib/helpers').wait;
3 |
4 | var QUEUE_NAME = 'amqp-connection-manager-sample1'
5 |
6 | // Create a connetion manager
7 | var connection = amqp.connect(['amqp://localhost']);
8 | connection.on('connect', function() {
9 | console.log('Connected!');
10 | });
11 | connection.on('disconnect', function(err) {
12 | console.log('Disconnected.', err.stack);
13 | });
14 |
15 | // Create a channel wrapper
16 | var channelWrapper = connection.createChannel({
17 | json: true,
18 | setup: function(channel) {
19 | // `channel` here is a regular amqplib `ConfirmChannel`.
20 | return channel.assertQueue(QUEUE_NAME, {durable: true});
21 | }
22 | });
23 |
24 | // Send messages until someone hits CTRL-C or something goes wrong...
25 | var sendMessage = function() {
26 | channelWrapper.sendToQueue(QUEUE_NAME, {time: Date.now()})
27 | .then(function() {
28 | console.log("Message sent");
29 | return wait(1000);
30 | })
31 | .then(function() {
32 | return sendMessage();
33 | }).catch(function(err) {
34 | console.log("Message was rejected:", err.stack);
35 | channelWrapper.close();
36 | connection.close();
37 | });
38 | };
39 |
40 | console.log("Sending messages...");
41 | sendMessage();
42 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // Transforms tell jest how to process our non-javascript files.
3 | transform: {
4 | '^.+\\.tsx?$': 'ts-jest',
5 | },
6 | // Tells Jest what folders to ignore for tests
7 | testPathIgnorePatterns: [`node_modules`, `\\.cache`],
8 | testMatch: ['**/test/**/*Test.ts'],
9 | collectCoverageFrom: ['**/src/**.ts', '!**/node_modules/**', '!**/vendor/**'],
10 | resolver: 'jest-ts-webcompat-resolver',
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "amqp-connection-manager",
3 | "version": "4.1.14",
4 | "description": "Auto-reconnect and round robin support for amqplib.",
5 | "module": "./dist/esm/index.js",
6 | "main": "./dist/cjs/index.js",
7 | "types": "./dist/types/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/types/index.d.ts",
11 | "import": "./dist/esm/index.js",
12 | "require": "./dist/cjs/index.js",
13 | "default": "./dist/cjs/index.js"
14 | }
15 | },
16 | "files": [
17 | "dist/**/*"
18 | ],
19 | "dependencies": {
20 | "promise-breaker": "^6.0.0"
21 | },
22 | "peerDependencies": {
23 | "amqplib": "*"
24 | },
25 | "devDependencies": {
26 | "@jwalton/semantic-release-config": "^1.0.0",
27 | "@semantic-release/changelog": "^6.0.1",
28 | "@semantic-release/git": "^10.0.1",
29 | "@types/amqplib": "^0.10.0",
30 | "@types/chai": "^4.2.21",
31 | "@types/chai-as-promised": "^7.1.4",
32 | "@types/chai-string": "^1.4.2",
33 | "@types/jest": "^29.2.0",
34 | "@types/node": "^20.0.0",
35 | "@types/whatwg-url": "^11.0.0",
36 | "@typescript-eslint/eslint-plugin": "^5.9.0",
37 | "@typescript-eslint/parser": "^5.9.0",
38 | "amqplib": "^0.10.3",
39 | "chai": "^4.1.2",
40 | "chai-as-promised": "^7.1.1",
41 | "chai-jest": "^1.0.2",
42 | "chai-string": "^1.1.2",
43 | "coveralls": "^3.1.0",
44 | "cross-env": "^7.0.2",
45 | "eslint": "^8.6.0",
46 | "eslint-config-prettier": "^8.3.0",
47 | "eslint-plugin-promise": "^6.0.0",
48 | "greenkeeper-lockfile": "^1.14.0",
49 | "husky": "^8.0.1",
50 | "istanbul": "^0.4.0",
51 | "jest": "^29.2.1",
52 | "jest-ts-webcompat-resolver": "^1.0.0",
53 | "lint-staged": "^13.0.3",
54 | "prettier": "^2.3.2",
55 | "pretty-quick": "^3.1.1",
56 | "promise-tools": "^2.1.0",
57 | "semantic-release": "^21.0.2",
58 | "ts-jest": "^29.0.3",
59 | "ts-node": "^10.2.1",
60 | "typescript": "^5.0.3"
61 | },
62 | "engines": {
63 | "node": ">=10.0.0",
64 | "npm": ">5.0.0"
65 | },
66 | "scripts": {
67 | "prepare": "husky install && npm run build",
68 | "prepublishOnly": "npm run build",
69 | "build": "tsc && tsc -p tsconfig.cjs.json && tsc -p tsconfig.types.json && ./bin/build-types.sh",
70 | "clean": "rm -rf dist types coverage",
71 | "test": "npm run test:lint && npm run test:unittest",
72 | "test:unittest": "tsc -p test && jest --coverage",
73 | "test:lint": "eslint --ext .ts src test",
74 | "semantic-release": "semantic-release"
75 | },
76 | "repository": {
77 | "type": "git",
78 | "url": "https://github.com/jwalton/node-amqp-connection-manager"
79 | },
80 | "lint-staged": {
81 | "**/*.ts": [
82 | "eslint --ext .ts"
83 | ]
84 | },
85 | "keywords": [
86 | "amqp",
87 | "rabbitmq",
88 | "cluster",
89 | "amqplib"
90 | ],
91 | "author": "Jason Walton (https://github.com/jwalton)",
92 | "license": "MIT",
93 | "bugs": {
94 | "url": "https://github.com/jwalton/node-amqp-connection-manager/issues"
95 | },
96 | "homepage": "https://github.com/jwalton/node-amqp-connection-manager"
97 | }
98 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "@jwalton/semantic-release-config",
3 | "verifyConditions": [
4 | "@semantic-release/changelog",
5 | "@semantic-release/npm",
6 | "@semantic-release/git"
7 | ],
8 | "prepare": [
9 | "@semantic-release/changelog",
10 | "@semantic-release/npm",
11 | "@semantic-release/git"
12 | ]
13 | };
14 |
--------------------------------------------------------------------------------
/src/AmqpConnectionManager.ts:
--------------------------------------------------------------------------------
1 | import * as amqp from 'amqplib';
2 | import { EventEmitter, once } from 'events';
3 | import { TcpSocketConnectOpts } from 'net';
4 | import pb from 'promise-breaker';
5 | import { ConnectionOptions } from 'tls';
6 | import { URL } from 'url';
7 | import ChannelWrapper, { CreateChannelOpts } from './ChannelWrapper.js';
8 | import { wait } from './helpers.js';
9 |
10 | // Default heartbeat time.
11 | const HEARTBEAT_IN_SECONDS = 5;
12 |
13 | export type ConnectionUrl =
14 | | string
15 | | amqp.Options.Connect
16 | | { url: string; connectionOptions?: AmqpConnectionOptions };
17 |
18 | export interface ConnectListener {
19 | (arg: { connection: amqp.Connection; url: string | amqp.Options.Connect }): void;
20 | }
21 |
22 | export interface ConnectFailedListener {
23 | (arg: { err: Error; url: string | amqp.Options.Connect | undefined }): void;
24 | }
25 |
26 | export type AmqpConnectionOptions = (ConnectionOptions | TcpSocketConnectOpts) & {
27 | noDelay?: boolean;
28 | timeout?: number;
29 | keepAlive?: boolean;
30 | keepAliveDelay?: number;
31 | clientProperties?: any;
32 | credentials?:
33 | | {
34 | mechanism: string;
35 | username: string;
36 | password: string;
37 | response: () => Buffer;
38 | }
39 | | {
40 | mechanism: string;
41 | response: () => Buffer;
42 | }
43 | | undefined;
44 | };
45 |
46 | export interface AmqpConnectionManagerOptions {
47 | /** Interval to send heartbeats to broker. Defaults to 5 seconds. */
48 | heartbeatIntervalInSeconds?: number;
49 |
50 | /**
51 | * The time to wait before trying to reconnect. If not specified, defaults
52 | * to `heartbeatIntervalInSeconds`.
53 | */
54 | reconnectTimeInSeconds?: number | undefined;
55 |
56 | /**
57 | * `findServers` is a function that which returns one or more servers to
58 | * connect to. This should return either a single URL or an array of URLs.
59 | * This is handy when you're using a service discovery mechanism such as
60 | * Consul or etcd. Instead of taking a callback, this can also return a
61 | * Promise. Note that if this is supplied, then `urls` is ignored.
62 | */
63 | findServers?:
64 | | ((callback: (urls: ConnectionUrl | ConnectionUrl[]) => void) => void)
65 | | (() => Promise)
66 | | undefined;
67 |
68 | /** Connection options, passed as options to the amqplib.connect() method. */
69 | connectionOptions?: AmqpConnectionOptions;
70 | }
71 |
72 | /* istanbul ignore next */
73 | function neverThrows() {
74 | return (err: Error) =>
75 | setImmediate(() => {
76 | throw new Error(
77 | `AmqpConnectionManager - should never get here: ${err.message}\n` + err.stack
78 | );
79 | });
80 | }
81 |
82 | export interface IAmqpConnectionManager {
83 | connectionOptions?: AmqpConnectionOptions;
84 | heartbeatIntervalInSeconds: number;
85 | reconnectTimeInSeconds: number;
86 |
87 | addListener(event: string, listener: (...args: any[]) => void): this;
88 | addListener(event: 'connect', listener: ConnectListener): this;
89 | addListener(event: 'connectFailed', listener: ConnectFailedListener): this;
90 | addListener(event: 'blocked', listener: (arg: { reason: string }) => void): this;
91 | addListener(event: 'unblocked', listener: () => void): this;
92 | addListener(event: 'disconnect', listener: (arg: { err: Error }) => void): this;
93 |
94 | // eslint-disable-next-line @typescript-eslint/ban-types
95 | listeners(eventName: string | symbol): Function[];
96 |
97 | on(event: string, listener: (...args: any[]) => void): this;
98 | on(event: 'connect', listener: ConnectListener): this;
99 | on(event: 'connectFailed', listener: ConnectFailedListener): this;
100 | on(event: 'blocked', listener: (arg: { reason: string }) => void): this;
101 | on(event: 'unblocked', listener: () => void): this;
102 | on(event: 'disconnect', listener: (arg: { err: Error }) => void): this;
103 |
104 | once(event: string, listener: (...args: any[]) => void): this;
105 | once(event: 'connect', listener: ConnectListener): this;
106 | once(event: 'connectFailed', listener: ConnectFailedListener): this;
107 | once(event: 'blocked', listener: (arg: { reason: string }) => void): this;
108 | once(event: 'unblocked', listener: () => void): this;
109 | once(event: 'disconnect', listener: (arg: { err: Error }) => void): this;
110 |
111 | prependListener(event: string, listener: (...args: any[]) => void): this;
112 | prependListener(event: 'connect', listener: ConnectListener): this;
113 | prependListener(event: 'connectFailed', listener: ConnectFailedListener): this;
114 | prependListener(event: 'blocked', listener: (arg: { reason: string }) => void): this;
115 | prependListener(event: 'unblocked', listener: () => void): this;
116 | prependListener(event: 'disconnect', listener: (arg: { err: Error }) => void): this;
117 |
118 | prependOnceListener(event: string, listener: (...args: any[]) => void): this;
119 | prependOnceListener(event: 'connect', listener: ConnectListener): this;
120 | prependOnceListener(event: 'connectFailed', listener: ConnectFailedListener): this;
121 | prependOnceListener(event: 'blocked', listener: (arg: { reason: string }) => void): this;
122 | prependOnceListener(event: 'unblocked', listener: () => void): this;
123 | prependOnceListener(event: 'disconnect', listener: (arg: { err: Error }) => void): this;
124 |
125 | removeListener(event: string, listener: (...args: any[]) => void): this;
126 |
127 | connect(options?: { timeout?: number }): Promise;
128 | reconnect(): void;
129 | createChannel(options?: CreateChannelOpts): ChannelWrapper;
130 | close(): Promise;
131 | isConnected(): boolean;
132 |
133 | /** The current connection. */
134 | readonly connection: amqp.Connection | undefined;
135 |
136 | /** Returns the number of registered channels. */
137 | readonly channelCount: number;
138 | }
139 |
140 | //
141 | // Events:
142 | // * `connect({connection, url})` - Emitted whenever we connect to a broker.
143 | // * `connectFailed({err, url})` - Emitted whenever we fail to connect to a broker.
144 | // * `disconnect({err})` - Emitted whenever we disconnect from a broker.
145 | // * `blocked({reason})` - Emitted whenever connection is blocked by a broker.
146 | // * `unblocked()` - Emitted whenever connection is unblocked by a broker.
147 | //
148 | export default class AmqpConnectionManager extends EventEmitter implements IAmqpConnectionManager {
149 | private _channels: ChannelWrapper[];
150 | private _currentUrl: number;
151 | private _closed = false;
152 | private _cancelRetriesHandler?: () => void;
153 | private _connectPromise?: Promise;
154 | private _currentConnection?: amqp.Connection;
155 | private _findServers:
156 | | ((callback: (urls: ConnectionUrl | ConnectionUrl[]) => void) => void)
157 | | (() => Promise);
158 | private _urls?: ConnectionUrl[];
159 |
160 | public connectionOptions: AmqpConnectionOptions | undefined;
161 | public heartbeatIntervalInSeconds: number;
162 | public reconnectTimeInSeconds: number;
163 |
164 | /**
165 | * Create a new AmqplibConnectionManager.
166 | *
167 | * @param urls - An array of brokers to connect to.
168 | * Takes url strings or objects {url: string, connectionOptions?: object}
169 | * If present, a broker's [connectionOptions] will be used instead
170 | * of [options.connectionOptions] when passed to the amqplib connect method.
171 | * AmqplibConnectionManager will round-robin between them whenever it
172 | * needs to create a new connection.
173 | * @param [options={}] -
174 | * @param [options.heartbeatIntervalInSeconds=5] - The interval,
175 | * in seconds, to send heartbeats.
176 | * @param [options.reconnectTimeInSeconds] - The time to wait
177 | * before trying to reconnect. If not specified, defaults to
178 | * `heartbeatIntervalInSeconds`.
179 | * @param [options.connectionOptions] - Passed to the amqplib
180 | * connect method.
181 | * @param [options.findServers] - A `fn(callback)` or a `fn()`
182 | * which returns a Promise. This should resolve to one or more servers
183 | * to connect to, either a single URL or an array of URLs. This is handy
184 | * when you're using a service discovery mechanism such as Consul or etcd.
185 | * Note that if this is supplied, then `urls` is ignored.
186 | */
187 | constructor(
188 | urls: ConnectionUrl | ConnectionUrl[] | undefined | null,
189 | options: AmqpConnectionManagerOptions = {}
190 | ) {
191 | super();
192 | if (!urls && !options.findServers) {
193 | throw new Error('Must supply either `urls` or `findServers`');
194 | }
195 | this._channels = [];
196 |
197 | this._currentUrl = 0;
198 | this.connectionOptions = options.connectionOptions;
199 |
200 | this.heartbeatIntervalInSeconds =
201 | options.heartbeatIntervalInSeconds || options.heartbeatIntervalInSeconds === 0
202 | ? options.heartbeatIntervalInSeconds
203 | : HEARTBEAT_IN_SECONDS;
204 | this.reconnectTimeInSeconds =
205 | options.reconnectTimeInSeconds || this.heartbeatIntervalInSeconds;
206 |
207 | // There will be one listener per channel, and there could be a lot of channels, so disable warnings from node.
208 | this.setMaxListeners(0);
209 |
210 | this._findServers = options.findServers || (() => Promise.resolve(urls));
211 | }
212 |
213 | /**
214 | * Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
215 | * reconnect attempts will continue in the background.
216 | * @param [options={}] -
217 | * @param [options.timeout] - Time to wait for initial connect
218 | */
219 | async connect({ timeout }: { timeout?: number } = {}): Promise {
220 | this._connect();
221 |
222 | let reject: (reason?: any) => void;
223 | const onConnectFailed = ({ err }: { err: Error }) => {
224 | // Ignore disconnects caused bad credentials.
225 | if (err.message.includes('ACCESS-REFUSED') || err.message.includes('403')) {
226 | reject(err);
227 | }
228 | };
229 |
230 | let waitTimeout;
231 | if (timeout) {
232 | waitTimeout = wait(timeout);
233 | }
234 | try {
235 | await Promise.race([
236 | once(this, 'connect'),
237 | new Promise((_resolve, innerReject) => {
238 | reject = innerReject;
239 | this.on('connectFailed', onConnectFailed);
240 | }),
241 | ...(waitTimeout
242 | ? [
243 | waitTimeout.promise.then(() => {
244 | throw new Error('amqp-connection-manager: connect timeout');
245 | }),
246 | ]
247 | : []),
248 | ]);
249 | } finally {
250 | waitTimeout?.cancel();
251 | this.removeListener('connectFailed', onConnectFailed);
252 | }
253 | }
254 |
255 | // `options` here are any options that can be passed to ChannelWrapper.
256 | createChannel(options: CreateChannelOpts = {}): ChannelWrapper {
257 | const channel = new ChannelWrapper(this, options);
258 | this._channels.push(channel);
259 | channel.once('close', () => {
260 | this._channels = this._channels.filter((c) => c !== channel);
261 | });
262 | return channel;
263 | }
264 |
265 | close(): Promise {
266 | if (this._closed) {
267 | return Promise.resolve();
268 | }
269 | this._closed = true;
270 |
271 | if (this._cancelRetriesHandler) {
272 | this._cancelRetriesHandler();
273 | this._cancelRetriesHandler = undefined;
274 | }
275 |
276 | return Promise.resolve(this._connectPromise).then(() => {
277 | return Promise.all(this._channels.map((channel) => channel.close()))
278 | .catch(function () {
279 | // Ignore errors closing channels.
280 | })
281 | .then(() => {
282 | this._channels = [];
283 | if (this._currentConnection) {
284 | this._currentConnection.removeAllListeners('close');
285 | return this._currentConnection.close();
286 | } else {
287 | return null;
288 | }
289 | })
290 | .then(() => {
291 | this._currentConnection = undefined;
292 | });
293 | });
294 | }
295 |
296 | isConnected(): boolean {
297 | return !!this._currentConnection;
298 | }
299 |
300 | /** Force reconnect - noop unless connected */
301 | reconnect(): void {
302 | if (this._closed) {
303 | throw new Error('cannot reconnect after close');
304 | }
305 |
306 | // If we have a connection, close it and immediately connect again.
307 | // Wait for ordinary reconnect otherwise.
308 | if (this._currentConnection) {
309 | this._currentConnection.removeAllListeners();
310 | this._currentConnection
311 | .close()
312 | .catch(() => {
313 | // noop
314 | })
315 | .then(() => {
316 | this._currentConnection = undefined;
317 | this.emit('disconnect', { err: new Error('forced reconnect') });
318 | return this._connect();
319 | })
320 | .catch(neverThrows);
321 | }
322 | }
323 |
324 | /** The current connection. */
325 | get connection(): amqp.Connection | undefined {
326 | return this._currentConnection;
327 | }
328 |
329 | /** Returns the number of registered channels. */
330 | get channelCount(): number {
331 | return this._channels.length;
332 | }
333 |
334 | private _connect(): Promise {
335 | if (this._connectPromise) {
336 | return this._connectPromise;
337 | }
338 |
339 | if (this._closed || this.isConnected()) {
340 | return Promise.resolve(null);
341 | }
342 |
343 | let attemptedUrl: string | amqp.Options.Connect | undefined;
344 |
345 | const result = (this._connectPromise = Promise.resolve()
346 | .then(() => {
347 | if (!this._urls || this._currentUrl >= this._urls.length) {
348 | this._currentUrl = 0;
349 | return pb.call(this._findServers, 0, null);
350 | } else {
351 | return this._urls;
352 | }
353 | })
354 | .then((urls: ConnectionUrl | ConnectionUrl[] | undefined) => {
355 | if (Array.isArray(urls)) {
356 | this._urls = urls;
357 | } else if (urls) {
358 | this._urls = [urls];
359 | }
360 |
361 | if (!this._urls || this._urls.length === 0) {
362 | throw new Error('amqp-connection-manager: No servers found');
363 | }
364 |
365 | // Round robin between brokers
366 | const url = this._urls[this._currentUrl];
367 | this._currentUrl++;
368 |
369 | // Set connectionOptions to the setting in the class instance (which came via the constructor)
370 | let connectionOptions: ConnectionOptions | undefined = this.connectionOptions;
371 | let originalUrl: string | amqp.Options.Connect;
372 | let connect: string | amqp.Options.Connect;
373 |
374 | if (typeof url === 'object' && 'url' in url) {
375 | originalUrl = connect = url.url;
376 | // If URL is an object, pull out any specific URL connectionOptions for it or use the
377 | // instance connectionOptions if none were provided for this specific URL.
378 | connectionOptions = url.connectionOptions || this.connectionOptions;
379 | } else if (typeof url === 'string') {
380 | originalUrl = connect = url;
381 | } else {
382 | originalUrl = url;
383 | connect = {
384 | ...url,
385 | heartbeat: url.heartbeat ?? this.heartbeatIntervalInSeconds,
386 | };
387 | }
388 | attemptedUrl = originalUrl;
389 |
390 | // Add the `heartbeastIntervalInSeconds` to the connection options.
391 | if (typeof connect === 'string') {
392 | const u = new URL(connect);
393 | if (!u.searchParams.get('heartbeat')) {
394 | u.searchParams.set('heartbeat', `${this.heartbeatIntervalInSeconds}`);
395 | }
396 | connect = u.toString();
397 | }
398 |
399 | return amqp.connect(connect, connectionOptions).then((connection) => {
400 | this._currentConnection = connection;
401 |
402 | //emit 'blocked' when RabbitMQ server decides to block the connection (resources running low)
403 | connection.on('blocked', (reason) => this.emit('blocked', { reason }));
404 |
405 | connection.on('unblocked', () => this.emit('unblocked'));
406 |
407 | connection.on('error', (/* err */) => {
408 | // if this event was emitted, then the connection was already closed,
409 | // so no need to call #close here
410 | // also, 'close' is emitted after 'error',
411 | // so no need for work already done in 'close' handler
412 | });
413 |
414 | // Reconnect if the connection closes
415 | connection.on('close', (err) => {
416 | this._currentConnection = undefined;
417 | this.emit('disconnect', { err });
418 |
419 | const handle = wait(this.reconnectTimeInSeconds * 1000);
420 | this._cancelRetriesHandler = handle.cancel;
421 |
422 | handle.promise
423 | .then(() => this._connect())
424 | // `_connect()` should never throw.
425 | .catch(neverThrows);
426 | });
427 |
428 | this._connectPromise = undefined;
429 | this.emit('connect', { connection, url: originalUrl });
430 |
431 | // Need to return null here, or Bluebird will complain - #171.
432 | return null;
433 | });
434 | })
435 | .catch((err) => {
436 | this.emit('connectFailed', { err, url: attemptedUrl });
437 |
438 | // Connection failed...
439 | this._currentConnection = undefined;
440 | this._connectPromise = undefined;
441 |
442 | let handle;
443 | if (err.name === 'OperationalError' && err.message === 'connect ETIMEDOUT') {
444 | handle = wait(0);
445 | } else {
446 | handle = wait(this.reconnectTimeInSeconds * 1000);
447 | }
448 | this._cancelRetriesHandler = handle.cancel;
449 |
450 | return handle.promise.then(() => this._connect());
451 | }));
452 |
453 | return result;
454 | }
455 | }
456 |
--------------------------------------------------------------------------------
/src/ChannelWrapper.ts:
--------------------------------------------------------------------------------
1 | import type * as amqplib from 'amqplib';
2 | import { Options } from 'amqplib';
3 | import * as crypto from 'crypto';
4 | import { EventEmitter } from 'events';
5 | import pb from 'promise-breaker';
6 | import { promisify } from 'util';
7 | import { IAmqpConnectionManager } from './AmqpConnectionManager.js';
8 |
9 | const MAX_MESSAGES_PER_BATCH = 1000;
10 |
11 | const randomBytes = promisify(crypto.randomBytes);
12 |
13 | export type Channel = amqplib.ConfirmChannel | amqplib.Channel;
14 |
15 | export type SetupFunc =
16 | | ((channel: Channel, callback: (error?: Error) => void) => void)
17 | | ((channel: Channel) => Promise)
18 | | ((channel: amqplib.ConfirmChannel, callback: (error?: Error) => void) => void)
19 | | ((channel: amqplib.ConfirmChannel) => Promise);
20 |
21 | export interface CreateChannelOpts {
22 | /** Name for this channel. Used for debugging. */
23 | name?: string;
24 | /**
25 | * A function to call whenever we reconnect to the broker (and therefore create a new underlying channel.)
26 | * This function should either accept a callback, or return a Promise. See addSetup below
27 | */
28 | setup?: SetupFunc;
29 | /**
30 | * True to create a ConfirmChannel (default). False to create a regular Channel.
31 | */
32 | confirm?: boolean;
33 | /**
34 | * if true, then ChannelWrapper assumes all messages passed to publish() and sendToQueue() are plain JSON objects.
35 | * These will be encoded automatically before being sent.
36 | */
37 | json?: boolean;
38 | /**
39 | * Default publish timeout in ms. Messages not published within the given time are rejected with a timeout error.
40 | */
41 | publishTimeout?: number;
42 | }
43 |
44 | interface PublishMessage {
45 | type: 'publish';
46 | exchange: string;
47 | routingKey: string;
48 | content: Buffer;
49 | options?: amqplib.Options.Publish;
50 | resolve: (result: boolean) => void;
51 | reject: (err: Error) => void;
52 | timeout?: NodeJS.Timeout;
53 | isTimedout: boolean;
54 | }
55 |
56 | interface SendToQueueMessage {
57 | type: 'sendToQueue';
58 | queue: string;
59 | content: Buffer;
60 | options?: amqplib.Options.Publish;
61 | resolve: (result: boolean) => void;
62 | reject: (err: Error) => void;
63 | timeout?: NodeJS.Timeout;
64 | isTimedout: boolean;
65 | }
66 |
67 | export interface PublishOptions extends Options.Publish {
68 | /** Message will be rejected after timeout ms */
69 | timeout?: number;
70 | }
71 |
72 | export interface ConsumerOptions extends amqplib.Options.Consume {
73 | prefetch?: number;
74 | }
75 |
76 | export interface Consumer {
77 | consumerTag: string | null;
78 | queue: string;
79 | onMessage: (msg: amqplib.ConsumeMessage) => void;
80 | options: ConsumerOptions;
81 | }
82 |
83 | type Message = PublishMessage | SendToQueueMessage;
84 |
85 | const IRRECOVERABLE_ERRORS = [
86 | 403, // AMQP Access Refused Error.
87 | 404, // AMQP Not Found Error.
88 | 406, // AMQP Precondition Failed Error.
89 | 501, // AMQP Frame Error.
90 | 502, // AMQP Frame Syntax Error.
91 | 503, // AMQP Invalid Command Error.
92 | 504, // AMQP Channel Not Open Error.
93 | 505, // AMQP Unexpected Frame.
94 | 530, // AMQP Not Allowed Error.
95 | 540, // AMQP Not Implemented Error.
96 | 541, // AMQP Internal Error.
97 | ];
98 |
99 | /**
100 | * Calls to `publish()` or `sendToQueue()` work just like in amqplib, but messages are queued internally and
101 | * are guaranteed to be delivered. If the underlying connection drops, ChannelWrapper will wait for a new
102 | * connection and continue.
103 | *
104 | * Events:
105 | * * `connect` - emitted every time this channel connects or reconnects.
106 | * * `error(err, {name})` - emitted if an error occurs setting up the channel.
107 | * * `drop({message, err})` - called when a JSON message was dropped because it could not be encoded.
108 | * * `close` - emitted when this channel closes via a call to `close()`
109 | *
110 | */
111 | export default class ChannelWrapper extends EventEmitter {
112 | private _connectionManager: IAmqpConnectionManager;
113 | private _json: boolean;
114 |
115 | /** If we're in the process of creating a channel, this is a Promise which
116 | * will resolve when the channel is set up. Otherwise, this is `null`.
117 | */
118 | private _settingUp: Promise | undefined = undefined;
119 | private _setups: SetupFunc[];
120 | /** Queued messages, not yet sent. */
121 | private _messages: Message[] = [];
122 | /** Oublished, but not yet confirmed messages. */
123 | private _unconfirmedMessages: Message[] = [];
124 | /** Reason code during publish or sendtoqueue messages. */
125 | private _irrecoverableCode: number | undefined;
126 | /** Consumers which will be reconnected on channel errors etc. */
127 | private _consumers: Consumer[] = [];
128 |
129 | /**
130 | * The currently connected channel. Note that not all setup functions
131 | * have been run on this channel until `@_settingUp` is either null or
132 | * resolved.
133 | */
134 | private _channel?: Channel;
135 |
136 | /**
137 | * True to create a ConfirmChannel. False to create a regular Channel.
138 | */
139 | private _confirm = true;
140 | /**
141 | * True if the "worker" is busy sending messages. False if we need to
142 | * start the worker to get stuff done.
143 | */
144 | private _working = false;
145 |
146 | /**
147 | * We kill off workers when we disconnect. Whenever we start a new
148 | * worker, we bump up the `_workerNumber` - this makes it so if stale
149 | * workers ever do wake up, they'll know to stop working.
150 | */
151 | private _workerNumber = 0;
152 |
153 | /**
154 | * True if the underlying channel has room for more messages.
155 | */
156 | private _channelHasRoom = true;
157 |
158 | /**
159 | * Default publish timeout
160 | */
161 | private _publishTimeout?: number;
162 |
163 | public name?: string;
164 |
165 | addListener(event: string, listener: (...args: any[]) => void): this;
166 | addListener(event: 'connect', listener: () => void): this;
167 | addListener(event: 'error', listener: (err: Error, info: { name: string }) => void): this;
168 | addListener(event: 'close', listener: () => void): this;
169 | addListener(event: string, listener: (...args: any[]) => void): this {
170 | return super.addListener(event, listener);
171 | }
172 |
173 | on(event: string, listener: (...args: any[]) => void): this;
174 | on(event: 'connect', listener: () => void): this;
175 | on(event: 'error', listener: (err: Error, info: { name: string }) => void): this;
176 | on(event: 'close', listener: () => void): this;
177 | on(event: string, listener: (...args: any[]) => void): this {
178 | return super.on(event, listener);
179 | }
180 |
181 | once(event: string, listener: (...args: any[]) => void): this;
182 | once(event: 'connect', listener: () => void): this;
183 | once(event: 'error', listener: (err: Error, info: { name: string }) => void): this;
184 | once(event: 'close', listener: () => void): this;
185 | once(event: string, listener: (...args: any[]) => void): this {
186 | return super.once(event, listener);
187 | }
188 |
189 | prependListener(event: string, listener: (...args: any[]) => void): this;
190 | prependListener(event: 'connect', listener: () => void): this;
191 | prependListener(event: 'error', listener: (err: Error, info: { name: string }) => void): this;
192 | prependListener(event: 'close', listener: () => void): this;
193 | prependListener(event: string, listener: (...args: any[]) => void): this {
194 | return super.prependListener(event, listener);
195 | }
196 |
197 | prependOnceListener(event: string, listener: (...args: any[]) => void): this;
198 | prependOnceListener(event: 'connect', listener: () => void): this;
199 | prependOnceListener(
200 | event: 'error',
201 | listener: (err: Error, info: { name: string }) => void
202 | ): this;
203 | prependOnceListener(event: 'close', listener: () => void): this;
204 | prependOnceListener(event: string, listener: (...args: any[]) => void): this {
205 | return super.prependOnceListener(event, listener);
206 | }
207 |
208 | /**
209 | * Adds a new 'setup handler'.
210 | *
211 | * `setup(channel, [cb])` is a function to call when a new underlying channel is created - handy for asserting
212 | * exchanges and queues exists, and whatnot. The `channel` object here is a ConfigChannel from amqplib.
213 | * The `setup` function should return a Promise (or optionally take a callback) - no messages will be sent until
214 | * this Promise resolves.
215 | *
216 | * If there is a connection, `setup()` will be run immediately, and the addSetup Promise/callback won't resolve
217 | * until `setup` is complete. Note that in this case, if the setup throws an error, no 'error' event will
218 | * be emitted, since you can just handle the error here (although the `setup` will still be added for future
219 | * reconnects, even if it throws an error.)
220 | *
221 | * Setup functions should, ideally, not throw errors, but if they do then the ChannelWrapper will emit an 'error'
222 | * event.
223 | *
224 | * @param setup - setup function.
225 | * @param [done] - callback.
226 | * @returns - Resolves when complete.
227 | */
228 | addSetup(setup: SetupFunc, done?: pb.Callback): Promise {
229 | return pb.addCallback(
230 | done,
231 | (this._settingUp || Promise.resolve()).then(() => {
232 | this._setups.push(setup);
233 | if (this._channel) {
234 | return pb.call(setup, this, this._channel);
235 | } else {
236 | return undefined;
237 | }
238 | })
239 | );
240 | }
241 |
242 | /**
243 | * Remove a setup function added with `addSetup`. If there is currently a
244 | * connection, `teardown(channel, [cb])` will be run immediately, and the
245 | * returned Promise will not resolve until it completes.
246 | *
247 | * @param {function} setup - the setup function to remove.
248 | * @param {function} [teardown] - `function(channel, [cb])` to run to tear
249 | * down the channel.
250 | * @param {function} [done] - Optional callback.
251 | * @returns {void | Promise} - Resolves when complete.
252 | */
253 | removeSetup(setup: SetupFunc, teardown?: SetupFunc, done?: pb.Callback): Promise {
254 | return pb.addCallback(done, () => {
255 | this._setups = this._setups.filter((s) => s !== setup);
256 |
257 | return (this._settingUp || Promise.resolve()).then(() =>
258 | this._channel && teardown ? pb.call(teardown, this, this._channel) : undefined
259 | );
260 | });
261 | }
262 |
263 | /**
264 | * Returns a Promise which resolves when this channel next connects.
265 | * (Mainly here for unit testing...)
266 | *
267 | * @param [done] - Optional callback.
268 | * @returns - Resolves when connected.
269 | */
270 | waitForConnect(done?: pb.Callback): Promise {
271 | return pb.addCallback(
272 | done,
273 | this._channel && !this._settingUp
274 | ? Promise.resolve()
275 | : new Promise((resolve) => this.once('connect', resolve))
276 | );
277 | }
278 |
279 | /*
280 | * Publish a message to the channel.
281 | *
282 | * This works just like amqplib's `publish()`, except if the channel is not
283 | * connected, this will wait until the channel is connected. Returns a
284 | * Promise which will only resolve when the message has been succesfully sent.
285 | * The returned promise will be rejected if `close()` is called on this
286 | * channel before it can be sent, if `options.json` is set and the message
287 | * can't be encoded, or if the broker rejects the message for some reason.
288 | *
289 | */
290 | publish(
291 | exchange: string,
292 | routingKey: string,
293 | content: Buffer | string | unknown,
294 | options?: PublishOptions,
295 | done?: pb.Callback
296 | ): Promise {
297 | return pb.addCallback(
298 | done,
299 | new Promise((resolve, reject) => {
300 | const { timeout, ...opts } = options || {};
301 | this._enqueueMessage(
302 | {
303 | type: 'publish',
304 | exchange,
305 | routingKey,
306 | content: this._getEncodedMessage(content),
307 | resolve,
308 | reject,
309 | options: opts,
310 | isTimedout: false,
311 | },
312 | timeout || this._publishTimeout
313 | );
314 | this._startWorker();
315 | })
316 | );
317 | }
318 |
319 | /*
320 | * Send a message to a queue.
321 | *
322 | * This works just like amqplib's `sendToQueue`, except if the channel is not connected, this will wait until the
323 | * channel is connected. Returns a Promise which will only resolve when the message has been succesfully sent.
324 | * The returned promise will be rejected only if `close()` is called on this channel before it can be sent.
325 | *
326 | * `message` here should be a JSON-able object.
327 | */
328 | sendToQueue(
329 | queue: string,
330 | content: Buffer | string | unknown,
331 | options?: PublishOptions,
332 | done?: pb.Callback
333 | ): Promise {
334 | const encodedContent = this._getEncodedMessage(content);
335 |
336 | return pb.addCallback(
337 | done,
338 | new Promise((resolve, reject) => {
339 | const { timeout, ...opts } = options || {};
340 | this._enqueueMessage(
341 | {
342 | type: 'sendToQueue',
343 | queue,
344 | content: encodedContent,
345 | resolve,
346 | reject,
347 | options: opts,
348 | isTimedout: false,
349 | },
350 | timeout || this._publishTimeout
351 | );
352 | this._startWorker();
353 | })
354 | );
355 | }
356 |
357 | private _enqueueMessage(message: Message, timeout?: number) {
358 | if (timeout) {
359 | message.timeout = setTimeout(() => {
360 | let idx = this._messages.indexOf(message);
361 | if (idx !== -1) {
362 | this._messages.splice(idx, 1);
363 | } else {
364 | idx = this._unconfirmedMessages.indexOf(message);
365 | if (idx !== -1) {
366 | this._unconfirmedMessages.splice(idx, 1);
367 | }
368 | }
369 | message.isTimedout = true;
370 | message.reject(new Error('timeout'));
371 | }, timeout);
372 | }
373 | this._messages.push(message);
374 | }
375 |
376 | /**
377 | * Create a new ChannelWrapper.
378 | *
379 | * @param connectionManager - connection manager which
380 | * created this channel.
381 | * @param [options] -
382 | * @param [options.name] - A name for this channel. Handy for debugging.
383 | * @param [options.setup] - A default setup function to call. See
384 | * `addSetup` for details.
385 | * @param [options.json] - if true, then ChannelWrapper assumes all
386 | * messages passed to `publish()` and `sendToQueue()` are plain JSON objects.
387 | * These will be encoded automatically before being sent.
388 | *
389 | */
390 | constructor(connectionManager: IAmqpConnectionManager, options: CreateChannelOpts = {}) {
391 | super();
392 | this._onConnect = this._onConnect.bind(this);
393 | this._onDisconnect = this._onDisconnect.bind(this);
394 | this._connectionManager = connectionManager;
395 | this._confirm = options.confirm ?? true;
396 | this.name = options.name;
397 |
398 | this._publishTimeout = options.publishTimeout;
399 | this._json = options.json ?? false;
400 |
401 | // Array of setup functions to call.
402 | this._setups = [];
403 | this._consumers = [];
404 |
405 | if (options.setup) {
406 | this._setups.push(options.setup);
407 | }
408 |
409 | const connection = connectionManager.connection;
410 | if (connection) {
411 | this._onConnect({ connection });
412 | }
413 | connectionManager.on('connect', this._onConnect);
414 | connectionManager.on('disconnect', this._onDisconnect);
415 | }
416 |
417 | // Called whenever we connect to the broker.
418 | private async _onConnect({ connection }: { connection: amqplib.Connection }): Promise {
419 | this._irrecoverableCode = undefined;
420 |
421 | try {
422 | let channel: Channel;
423 | if (this._confirm) {
424 | channel = await connection.createConfirmChannel();
425 | } else {
426 | channel = await connection.createChannel();
427 | }
428 |
429 | this._channel = channel;
430 | this._channelHasRoom = true;
431 | channel.on('close', () => this._onChannelClose(channel));
432 | channel.on('drain', () => this._onChannelDrain());
433 |
434 | this._settingUp = Promise.all(
435 | this._setups.map((setupFn) =>
436 | // TODO: Use a timeout here to guard against setupFns that never resolve?
437 | pb.call(setupFn, this, channel).catch((err) => {
438 | if (err.name === 'IllegalOperationError') {
439 | // Don't emit an error if setups failed because the channel closed.
440 | return;
441 | }
442 | this.emit('error', err, { name: this.name });
443 | })
444 | )
445 | )
446 | .then(() => {
447 | return Promise.all(this._consumers.map((c) => this._reconnectConsumer(c)));
448 | })
449 | .then(() => {
450 | this._settingUp = undefined;
451 | });
452 | await this._settingUp;
453 |
454 | if (!this._channel) {
455 | // Can happen if channel closes while we're setting up.
456 | return;
457 | }
458 |
459 | // Since we just connected, publish any queued messages
460 | this._startWorker();
461 | this.emit('connect');
462 | } catch (err) {
463 | this.emit('error', err, { name: this.name });
464 | this._settingUp = undefined;
465 | this._channel = undefined;
466 | }
467 | }
468 |
469 | // Called whenever the channel closes.
470 | private _onChannelClose(channel: Channel): void {
471 | if (this._channel === channel) {
472 | this._channel = undefined;
473 | }
474 | // Wait for another reconnect to create a new channel.
475 | }
476 |
477 | /** Called whenever the channel drains. */
478 | private _onChannelDrain(): void {
479 | this._channelHasRoom = true;
480 | this._startWorker();
481 | }
482 |
483 | // Called whenever we disconnect from the AMQP server.
484 | private _onDisconnect(ex: { err: Error & { code: number } }): void {
485 | this._irrecoverableCode = ex.err instanceof Error ? ex.err.code : undefined;
486 | this._channel = undefined;
487 | this._settingUp = undefined;
488 |
489 | // Kill off the current worker. We never get any kind of error for messages in flight - see
490 | // https://github.com/squaremo/amqp.node/issues/191.
491 | this._working = false;
492 | }
493 |
494 | // Returns the number of unsent messages queued on this channel.
495 | queueLength(): number {
496 | return this._messages.length;
497 | }
498 |
499 | // Destroy this channel.
500 | //
501 | // Any unsent messages will have their associated Promises rejected.
502 | //
503 | close(): Promise {
504 | return Promise.resolve().then(() => {
505 | this._working = false;
506 | if (this._messages.length !== 0) {
507 | // Reject any unsent messages.
508 | this._messages.forEach((message) => {
509 | if (message.timeout) {
510 | clearTimeout(message.timeout);
511 | }
512 | message.reject(new Error('Channel closed'));
513 | });
514 | }
515 | if (this._unconfirmedMessages.length !== 0) {
516 | // Reject any unconfirmed messages.
517 | this._unconfirmedMessages.forEach((message) => {
518 | if (message.timeout) {
519 | clearTimeout(message.timeout);
520 | }
521 | message.reject(new Error('Channel closed'));
522 | });
523 | }
524 |
525 | this._connectionManager.removeListener('connect', this._onConnect);
526 | this._connectionManager.removeListener('disconnect', this._onDisconnect);
527 | const answer = (this._channel && this._channel.close()) || undefined;
528 | this._channel = undefined;
529 |
530 | this.emit('close');
531 |
532 | return answer;
533 | });
534 | }
535 |
536 | private _shouldPublish(): boolean {
537 | return (
538 | this._messages.length > 0 && !this._settingUp && !!this._channel && this._channelHasRoom
539 | );
540 | }
541 |
542 | // Start publishing queued messages, if there isn't already a worker doing this.
543 | private _startWorker(): void {
544 | if (!this._working && this._shouldPublish()) {
545 | this._working = true;
546 | this._workerNumber++;
547 | this._publishQueuedMessages(this._workerNumber);
548 | }
549 | }
550 |
551 | // Define if a message can cause irrecoverable error
552 | private _canWaitReconnection(): boolean {
553 | return !this._irrecoverableCode || !IRRECOVERABLE_ERRORS.includes(this._irrecoverableCode);
554 | }
555 |
556 | private _messageResolved(message: Message, result: boolean) {
557 | removeUnconfirmedMessage(this._unconfirmedMessages, message);
558 | message.resolve(result);
559 | }
560 |
561 | private _messageRejected(message: Message, err: Error) {
562 | if (!this._channel && this._canWaitReconnection()) {
563 | // Tried to write to a closed channel. Leave the message in the queue and we'll try again when
564 | // we reconnect.
565 | removeUnconfirmedMessage(this._unconfirmedMessages, message);
566 | this._messages.push(message);
567 | } else {
568 | // Something went wrong trying to send this message - could be JSON.stringify failed, could be
569 | // the broker rejected the message. Either way, reject it back
570 | removeUnconfirmedMessage(this._unconfirmedMessages, message);
571 | message.reject(err);
572 | }
573 | }
574 |
575 | private _getEncodedMessage(content: Buffer | string | unknown): Buffer {
576 | let encodedMessage: Buffer;
577 |
578 | if (this._json) {
579 | encodedMessage = Buffer.from(JSON.stringify(content));
580 | } else if (typeof content === 'string') {
581 | encodedMessage = Buffer.from(content);
582 | } else if (content instanceof Buffer) {
583 | encodedMessage = content;
584 | } else if (typeof content === 'object' && typeof (content as any).toString === 'function') {
585 | encodedMessage = Buffer.from((content as any).toString());
586 | } else {
587 | console.warn(
588 | 'amqp-connection-manager: Sending JSON message, but json option not speicifed'
589 | );
590 | encodedMessage = Buffer.from(JSON.stringify(content));
591 | }
592 |
593 | return encodedMessage;
594 | }
595 |
596 | private _publishQueuedMessages(workerNumber: number): void {
597 | const channel = this._channel;
598 | if (
599 | !channel ||
600 | !this._shouldPublish() ||
601 | !this._working ||
602 | workerNumber !== this._workerNumber
603 | ) {
604 | // Can't publish anything right now...
605 | this._working = false;
606 | return;
607 | }
608 |
609 | try {
610 | // Send messages in batches of 1000 - don't want to starve the event loop.
611 | let sendsLeft = MAX_MESSAGES_PER_BATCH;
612 | while (this._channelHasRoom && this._messages.length > 0 && sendsLeft > 0) {
613 | sendsLeft--;
614 |
615 | const message = this._messages.shift();
616 | if (!message) {
617 | break;
618 | }
619 |
620 | let thisCanSend = true;
621 |
622 | switch (message.type) {
623 | case 'publish': {
624 | if (this._confirm) {
625 | this._unconfirmedMessages.push(message);
626 | thisCanSend = this._channelHasRoom = channel.publish(
627 | message.exchange,
628 | message.routingKey,
629 | message.content,
630 | message.options,
631 | (err) => {
632 | if (message.isTimedout) {
633 | return;
634 | }
635 |
636 | if (message.timeout) {
637 | clearTimeout(message.timeout);
638 | }
639 |
640 | if (err) {
641 | this._messageRejected(message, err);
642 | } else {
643 | this._messageResolved(message, thisCanSend);
644 | }
645 | }
646 | );
647 | } else {
648 | if (message.timeout) {
649 | clearTimeout(message.timeout);
650 | }
651 | thisCanSend = this._channelHasRoom = channel.publish(
652 | message.exchange,
653 | message.routingKey,
654 | message.content,
655 | message.options
656 | );
657 | message.resolve(thisCanSend);
658 | }
659 | break;
660 | }
661 | case 'sendToQueue': {
662 | if (this._confirm) {
663 | this._unconfirmedMessages.push(message);
664 | thisCanSend = this._channelHasRoom = channel.sendToQueue(
665 | message.queue,
666 | message.content,
667 | message.options,
668 | (err) => {
669 | if (message.isTimedout) {
670 | return;
671 | }
672 |
673 | if (message.timeout) {
674 | clearTimeout(message.timeout);
675 | }
676 |
677 | if (err) {
678 | this._messageRejected(message, err);
679 | } else {
680 | this._messageResolved(message, thisCanSend);
681 | }
682 | }
683 | );
684 | } else {
685 | if (message.timeout) {
686 | clearTimeout(message.timeout);
687 | }
688 | thisCanSend = this._channelHasRoom = channel.sendToQueue(
689 | message.queue,
690 | message.content,
691 | message.options
692 | );
693 | message.resolve(thisCanSend);
694 | }
695 | break;
696 | }
697 | /* istanbul ignore next */
698 | default:
699 | throw new Error(`Unhandled message type ${(message as any).type}`);
700 | }
701 | }
702 |
703 | // If we didn't send all the messages, send some more...
704 | if (this._channelHasRoom && this._messages.length > 0) {
705 | setImmediate(() => this._publishQueuedMessages(workerNumber));
706 | } else {
707 | this._working = false;
708 | }
709 |
710 | /* istanbul ignore next */
711 | } catch (err) {
712 | this._working = false;
713 | this.emit('error', err);
714 | }
715 | }
716 |
717 | /**
718 | * Setup a consumer
719 | * This consumer will be reconnected on cancellation and channel errors.
720 | */
721 | async consume(
722 | queue: string,
723 | onMessage: Consumer['onMessage'],
724 | options: ConsumerOptions = {}
725 | ): Promise {
726 | const consumerTag = options.consumerTag || (await randomBytes(16)).toString('hex');
727 | const consumer: Consumer = {
728 | consumerTag: null,
729 | queue,
730 | onMessage,
731 | options: {
732 | ...options,
733 | consumerTag,
734 | },
735 | };
736 |
737 | if (this._settingUp) {
738 | await this._settingUp;
739 | }
740 |
741 | this._consumers.push(consumer);
742 | await this._consume(consumer);
743 | return { consumerTag };
744 | }
745 |
746 | private async _consume(consumer: Consumer): Promise {
747 | if (!this._channel) {
748 | return;
749 | }
750 |
751 | const { prefetch, ...options } = consumer.options;
752 | if (typeof prefetch === 'number') {
753 | this._channel.prefetch(prefetch, false);
754 | }
755 |
756 | const { consumerTag } = await this._channel.consume(
757 | consumer.queue,
758 | (msg) => {
759 | if (!msg) {
760 | consumer.consumerTag = null;
761 | this._reconnectConsumer(consumer).catch((err) => {
762 | if (err.code === 404) {
763 | // Ignore errors caused by queue not declared. In
764 | // those cases the connection will reconnect and
765 | // then consumers reestablished. The full reconnect
766 | // might be avoided if we assert the queue again
767 | // before starting to consume.
768 | return;
769 | }
770 | this.emit('error', err);
771 | });
772 | return;
773 | }
774 | consumer.onMessage(msg);
775 | },
776 | options
777 | );
778 | consumer.consumerTag = consumerTag;
779 | }
780 |
781 | private async _reconnectConsumer(consumer: Consumer): Promise {
782 | if (!this._consumers.includes(consumer)) {
783 | // Intentionally canceled
784 | return;
785 | }
786 | await this._consume(consumer);
787 | }
788 |
789 | /**
790 | * Cancel all consumers
791 | */
792 | async cancelAll(): Promise {
793 | const consumers = this._consumers;
794 | this._consumers = [];
795 | if (!this._channel) {
796 | return;
797 | }
798 |
799 | const channel = this._channel;
800 | await Promise.all(
801 | consumers.reduce((acc, consumer) => {
802 | if (consumer.consumerTag) {
803 | acc.push(channel.cancel(consumer.consumerTag));
804 | }
805 | return acc;
806 | }, [])
807 | );
808 | }
809 |
810 | async cancel(consumerTag: string): Promise {
811 | const idx = this._consumers.findIndex((x) => x.options.consumerTag === consumerTag);
812 | if (idx === -1) {
813 | return;
814 | }
815 |
816 | const consumer = this._consumers[idx];
817 | this._consumers.splice(idx, 1);
818 | if (this._channel && consumer.consumerTag) {
819 | await this._channel.cancel(consumer.consumerTag);
820 | }
821 | }
822 |
823 | /** Send an `ack` to the underlying channel. */
824 | ack(message: amqplib.Message, allUpTo?: boolean): void {
825 | this._channel && this._channel.ack(message, allUpTo);
826 | }
827 |
828 | /** Send an `ackAll` to the underlying channel. */
829 | ackAll(): void {
830 | this._channel && this._channel.ackAll();
831 | }
832 |
833 | /** Send a `nack` to the underlying channel. */
834 | nack(message: amqplib.Message, allUpTo?: boolean, requeue?: boolean): void {
835 | this._channel && this._channel.nack(message, allUpTo, requeue);
836 | }
837 |
838 | /** Send a `nackAll` to the underlying channel. */
839 | nackAll(requeue?: boolean): void {
840 | this._channel && this._channel.nackAll(requeue);
841 | }
842 |
843 | /** Send a `purgeQueue` to the underlying channel. */
844 | async purgeQueue(queue: string): Promise {
845 | if (this._channel) {
846 | return await this._channel.purgeQueue(queue);
847 | } else {
848 | throw new Error(`Not connected.`);
849 | }
850 | }
851 |
852 | /** Send a `checkQueue` to the underlying channel. */
853 | async checkQueue(queue: string): Promise {
854 | if (this._channel) {
855 | return await this._channel.checkQueue(queue);
856 | } else {
857 | throw new Error(`Not connected.`);
858 | }
859 | }
860 |
861 | /** Send a `assertQueue` to the underlying channel. */
862 | async assertQueue(
863 | queue: string,
864 | options?: amqplib.Options.AssertQueue
865 | ): Promise {
866 | if (this._channel) {
867 | return await this._channel.assertQueue(queue, options);
868 | } else {
869 | return { queue, messageCount: 0, consumerCount: 0 };
870 | }
871 | }
872 |
873 | /** Send a `bindQueue` to the underlying channel. */
874 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
875 | async bindQueue(queue: string, source: string, pattern: string, args?: any): Promise {
876 | if (this._channel) {
877 | await this._channel.bindQueue(queue, source, pattern, args);
878 | }
879 | }
880 |
881 | /** Send a `unbindQueue` to the underlying channel. */
882 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
883 | async unbindQueue(queue: string, source: string, pattern: string, args?: any): Promise {
884 | if (this._channel) {
885 | await this._channel.unbindQueue(queue, source, pattern, args);
886 | }
887 | }
888 |
889 | /** Send a `deleteQueue` to the underlying channel. */
890 | async deleteQueue(
891 | queue: string,
892 | options?: Options.DeleteQueue
893 | ): Promise {
894 | if (this._channel) {
895 | return await this._channel.deleteQueue(queue, options);
896 | } else {
897 | throw new Error(`Not connected.`);
898 | }
899 | }
900 |
901 | /** Send a `assertExchange` to the underlying channel. */
902 | async assertExchange(
903 | exchange: string,
904 | type: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string,
905 | options?: Options.AssertExchange
906 | ): Promise {
907 | if (this._channel) {
908 | return await this._channel.assertExchange(exchange, type, options);
909 | } else {
910 | return { exchange };
911 | }
912 | }
913 |
914 | /** Send a `bindExchange` to the underlying channel. */
915 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
916 | async bindExchange(
917 | destination: string,
918 | source: string,
919 | pattern: string,
920 | args?: any
921 | ): Promise {
922 | if (this._channel) {
923 | return await this._channel.bindExchange(destination, source, pattern, args);
924 | } else {
925 | throw new Error(`Not connected.`);
926 | }
927 | }
928 |
929 | /** Send a `checkExchange` to the underlying channel. */
930 | async checkExchange(exchange: string): Promise {
931 | if (this._channel) {
932 | return await this._channel.checkExchange(exchange);
933 | } else {
934 | throw new Error(`Not connected.`);
935 | }
936 | }
937 |
938 | /** Send a `deleteExchange` to the underlying channel. */
939 | async deleteExchange(
940 | exchange: string,
941 | options?: Options.DeleteExchange
942 | ): Promise {
943 | if (this._channel) {
944 | return await this._channel.deleteExchange(exchange, options);
945 | } else {
946 | throw new Error(`Not connected.`);
947 | }
948 | }
949 |
950 | /** Send a `unbindExchange` to the underlying channel. */
951 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
952 | async unbindExchange(
953 | destination: string,
954 | source: string,
955 | pattern: string,
956 | args?: any
957 | ): Promise {
958 | if (this._channel) {
959 | return await this._channel.unbindExchange(destination, source, pattern, args);
960 | } else {
961 | throw new Error(`Not connected.`);
962 | }
963 | }
964 |
965 | /** Send a `get` to the underlying channel. */
966 | async get(queue: string, options?: Options.Get): Promise {
967 | if (this._channel) {
968 | return await this._channel.get(queue, options);
969 | } else {
970 | throw new Error(`Not connected.`);
971 | }
972 | }
973 | }
974 |
975 | function removeUnconfirmedMessage(arr: Message[], message: Message) {
976 | const toRemove = arr.indexOf(message);
977 | if (toRemove === -1) {
978 | throw new Error(`Message is not in _unconfirmedMessages!`);
979 | }
980 | const removed = arr.splice(toRemove, 1);
981 | return removed[0];
982 | }
983 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | export function wait(timeInMs: number): { promise: Promise; cancel: () => void } {
2 | let timeoutHandle: NodeJS.Timeout;
3 |
4 | return {
5 | promise: new Promise(function (resolve) {
6 | timeoutHandle = setTimeout(resolve, timeInMs);
7 | }),
8 | cancel: () => clearTimeout(timeoutHandle),
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-namespace */
2 | import AmqpConnectionManager, {
3 | AmqpConnectionManagerOptions,
4 | ConnectionUrl,
5 | IAmqpConnectionManager,
6 | } from './AmqpConnectionManager.js';
7 | import CW, { PublishOptions } from './ChannelWrapper.js';
8 |
9 | export type {
10 | AmqpConnectionManagerOptions,
11 | ConnectionUrl,
12 | IAmqpConnectionManager as AmqpConnectionManager,
13 | } from './AmqpConnectionManager.js';
14 | export type { CreateChannelOpts, SetupFunc, Channel } from './ChannelWrapper.js';
15 | export type ChannelWrapper = CW;
16 |
17 | import { Options as AmqpLibOptions } from 'amqplib';
18 |
19 | export namespace Options {
20 | export type Connect = AmqpLibOptions.Connect;
21 | export type AssertQueue = AmqpLibOptions.AssertQueue;
22 | export type DeleteQueue = AmqpLibOptions.DeleteQueue;
23 | export type AssertExchange = AmqpLibOptions.AssertExchange;
24 | export type DeleteExchange = AmqpLibOptions.DeleteExchange;
25 | export type Publish = PublishOptions;
26 | export type Consume = AmqpLibOptions.Consume;
27 | export type Get = AmqpLibOptions.Get;
28 | }
29 |
30 | export function connect(
31 | urls: ConnectionUrl | ConnectionUrl[] | undefined | null,
32 | options?: AmqpConnectionManagerOptions
33 | ): IAmqpConnectionManager {
34 | const conn = new AmqpConnectionManager(urls, options);
35 | conn.connect().catch(() => {
36 | /* noop */
37 | });
38 | return conn;
39 | }
40 |
41 | export { AmqpConnectionManager as AmqpConnectionManagerClass };
42 |
43 | const amqp = { connect };
44 |
45 | export default amqp;
46 |
--------------------------------------------------------------------------------
/test/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'import/extensions': 'off',
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/test/AmqpConnectionManagerTest.ts:
--------------------------------------------------------------------------------
1 | import origAmqp from 'amqplib';
2 | import chai from 'chai';
3 | import chaiString from 'chai-string';
4 | import { once } from 'events';
5 | import * as promiseTools from 'promise-tools';
6 | import AmqpConnectionManager from '../src/AmqpConnectionManager';
7 | import { FakeAmqp, FakeConnection } from './fixtures';
8 |
9 | chai.use(chaiString);
10 | const { expect } = chai;
11 |
12 | const amqplib = new FakeAmqp();
13 |
14 | describe('AmqpConnectionManager', function () {
15 | let amqp: AmqpConnectionManager | undefined;
16 |
17 | beforeEach(() => {
18 | jest.spyOn(origAmqp, 'connect').mockImplementation(((url: string) =>
19 | amqplib.connect(url)) as any);
20 | amqplib.reset();
21 | });
22 |
23 | afterEach(() => {
24 | amqp?.close();
25 | jest.restoreAllMocks();
26 | });
27 |
28 | it('should establish a connection to a broker', async () => {
29 | amqp = new AmqpConnectionManager('amqp://localhost');
30 | amqp.connect();
31 | const [{ connection, url }] = await once(amqp, 'connect');
32 | expect(url, 'url').to.equal('amqp://localhost');
33 | expect(connection.url, 'connection.url').to.equal('amqp://localhost?heartbeat=5');
34 | });
35 |
36 | it('should establish a connection to a broker, using an object as the URL', async () => {
37 | amqp = new AmqpConnectionManager({
38 | protocol: 'amqp',
39 | hostname: 'localhost',
40 | });
41 | amqp.connect();
42 | const [{ connection, url }] = await once(amqp, 'connect');
43 | expect(url, 'url').to.eql({
44 | protocol: 'amqp',
45 | hostname: 'localhost',
46 | });
47 | expect((connection as any).url, 'connection.url').to.eql({
48 | protocol: 'amqp',
49 | hostname: 'localhost',
50 | heartbeat: 5,
51 | });
52 | });
53 |
54 | it('should establish a url object based connection to a broker', async () => {
55 | amqp = new AmqpConnectionManager({ url: 'amqp://localhost' });
56 | amqp.connect();
57 | const [{ connection, url }] = await once(amqp, 'connect');
58 | expect(url, 'url').to.equal('amqp://localhost');
59 | expect(connection.url, 'connection.url').to.equal('amqp://localhost?heartbeat=5');
60 | });
61 |
62 | it('should establish a connection to a broker disabling heartbeat', async () => {
63 | amqp = new AmqpConnectionManager('amqp://localhost', {
64 | heartbeatIntervalInSeconds: 0,
65 | });
66 | amqp.connect();
67 | const [{ connection, url }] = await once(amqp, 'connect');
68 | expect(url, 'url').to.equal('amqp://localhost');
69 | expect(connection.url, 'connection.url').to.equal('amqp://localhost?heartbeat=0');
70 | });
71 |
72 | it('should close connection to a broker', async () => {
73 | amqp = new AmqpConnectionManager('amqp://localhost');
74 | amqp.connect();
75 | const [{ connection, url }] = await once(amqp, 'connect');
76 | expect(url, 'url').to.equal('amqp://localhost');
77 | expect((connection as any).url, 'connection.url').to.equal('amqp://localhost?heartbeat=5');
78 | const conn = amqp.connection;
79 | await amqp?.close();
80 |
81 | expect(amqp?.connection, 'current connection').to.be.undefined;
82 | expect((conn as any)._closed, 'connection closed').to.be.true;
83 | });
84 |
85 | // /**
86 | // * When close() was called before _connect() finished, the connection was remaining established indefinitely
87 | // */
88 | it('should close pending connection to a broker', async () => {
89 | let closed = false;
90 | let connected = false;
91 |
92 | amqp = new AmqpConnectionManager('amqp://localhost');
93 | amqp.connect();
94 | // Connection should not yet be established
95 | expect(amqp.connection, 'current connection').to.equal(undefined);
96 | // Connection should be pending though
97 | expect((amqp as any)._connectPromise).to.be.an.instanceof(Promise);
98 |
99 | // Call close before the connection is established
100 | const closePromise = amqp.close().then(() => {
101 | closed = true;
102 |
103 | // Connection should not present after close
104 | expect(amqp?.connection, 'current connection').to.be.undefined;
105 | // Connection promise should not be present anymore
106 | expect((amqp as any)._connectPromise).to.be.undefined;
107 | // Connect should resolve before close
108 | expect(connected).to.equal(true);
109 | });
110 |
111 | // This prevents double call to close()
112 | expect((amqp as any)._closed).to.equal(true);
113 |
114 | // Wait for connect before checking amqp?.connection
115 | const connectPromise = new Promise((resolve, reject) => {
116 | // I tried to use once helper from events module but
117 | // does not work with babel for some reason
118 | amqp?.once('connect', resolve);
119 | amqp?.once('error', reject);
120 | }).then(() => {
121 | connected = true;
122 |
123 | // Connection should be present right after connect
124 | expect(amqp?.connection, 'current connection').to.be.an.instanceof(FakeConnection);
125 | // Connection promise should not be present anymore
126 | expect((amqp as any)._connectPromise).to.be.undefined;
127 | // Connect should resolve before close
128 | expect(closed).to.equal(false);
129 | });
130 |
131 | await Promise.all([closePromise, connectPromise]);
132 | });
133 |
134 | it('should establish a connection to a broker using findServers', async () => {
135 | amqp = new AmqpConnectionManager(null, {
136 | findServers() {
137 | return Promise.resolve('amqp://localhost');
138 | },
139 | });
140 | amqp.connect();
141 | const [{ connection, url }] = await once(amqp, 'connect');
142 | expect(url, 'url').to.equal('amqp://localhost');
143 | expect(connection.url, 'connection.url').to.equal('amqp://localhost?heartbeat=5');
144 | });
145 |
146 | it('should establish a url object based connection to a broker using findServers', async () => {
147 | amqp = new AmqpConnectionManager(null, {
148 | findServers() {
149 | return Promise.resolve({ url: 'amqp://localhost' });
150 | },
151 | });
152 | amqp.connect();
153 | const [{ connection, url }] = await once(amqp, 'connect');
154 | expect(url, 'url').to.equal('amqp://localhost');
155 | expect(connection.url, 'connection.url').to.equal('amqp://localhost?heartbeat=5');
156 | });
157 |
158 | it('should fail to connect if findServers returns no servers', async () => {
159 | amqp = new AmqpConnectionManager(null, {
160 | findServers() {
161 | return Promise.resolve(null);
162 | },
163 | });
164 |
165 | amqp.connect();
166 | const [{ err }] = await once(amqp, 'connectFailed');
167 | expect(err.message).to.contain('No servers found');
168 | return amqp?.close();
169 | });
170 |
171 | it('should timeout connect', async () => {
172 | jest.spyOn(origAmqp, 'connect').mockImplementation((): any => {
173 | return promiseTools.delay(200);
174 | });
175 | amqp = new AmqpConnectionManager('amqp://localhost');
176 | let err;
177 | try {
178 | await amqp.connect({ timeout: 0.1 });
179 | } catch (error: any) {
180 | err = error;
181 | }
182 | expect(err.message).to.equal('amqp-connection-manager: connect timeout');
183 | });
184 |
185 | it('should work with a URL with a query', async () => {
186 | amqp = new AmqpConnectionManager('amqp://localhost?frameMax=0x1000');
187 | amqp.connect();
188 | const [{ connection }] = await once(amqp, 'connect');
189 | expect(connection.url, 'connection.url').to.equal(
190 | 'amqp://localhost?frameMax=0x1000&heartbeat=5'
191 | );
192 | });
193 |
194 | it('should throw an error if no url and no `findServers` option are provided', async () => {
195 | expect(() => new (AmqpConnectionManager as any)()).to.throw(
196 | 'Must supply either `urls` or `findServers`'
197 | );
198 | });
199 |
200 | it("should reconnect to the broker if it can't connect in the first place", async () => {
201 | amqplib.deadServers = ['amqp://rabbit1'];
202 |
203 | // Should try to connect to rabbit1 first and be refused, and then successfully connect to rabbit2.
204 | amqp = new AmqpConnectionManager(['amqp://rabbit1', 'amqp://rabbit2'], {
205 | heartbeatIntervalInSeconds: 0.01,
206 | });
207 | amqp.connect();
208 |
209 | let connectFailedSeen = 0;
210 | amqp.on('connectFailed', function () {
211 | connectFailedSeen++;
212 | amqplib.failConnections = false;
213 | });
214 |
215 | const [{ connection, url }] = await once(amqp, 'connect');
216 | expect(connectFailedSeen).to.equal(1);
217 |
218 | // Verify that we round-robined to the next server, since the first was unavailable.
219 | expect(url, 'url').to.equal('amqp://rabbit2');
220 | if (typeof url !== 'string') {
221 | throw new Error('url is not a string');
222 | }
223 | expect((connection as any).url, 'connection.url').to.startWith(url);
224 | });
225 |
226 | it('should reconnect to the broker if the broker disconnects', async () => {
227 | amqp = new AmqpConnectionManager('amqp://localhost', {
228 | heartbeatIntervalInSeconds: 0.01,
229 | });
230 | let disconnectsSeen = 0;
231 | amqp.on('disconnect', () => disconnectsSeen++);
232 |
233 | await amqp.connect();
234 | amqplib.kill();
235 |
236 | await amqp.connect();
237 | expect(disconnectsSeen).to.equal(1);
238 | });
239 |
240 | it('should reconnect to the broker if the broker closed connection', async () => {
241 | amqp = new AmqpConnectionManager('amqp://localhost', {
242 | heartbeatIntervalInSeconds: 0.01,
243 | });
244 |
245 | let disconnectsSeen = 0;
246 | amqp.on('disconnect', () => disconnectsSeen++);
247 |
248 | await amqp.connect();
249 |
250 | // Close the connection nicely
251 | amqplib.simulateRemoteClose();
252 |
253 | await once(amqp, 'connect');
254 | expect(disconnectsSeen).to.equal(1);
255 | });
256 |
257 | it('should know if it is connected or not', async () => {
258 | amqp = new AmqpConnectionManager('amqp://localhost');
259 | amqp.connect();
260 |
261 | expect(amqp.isConnected()).to.be.false;
262 |
263 | await once(amqp, 'connect');
264 | expect(amqp?.isConnected()).to.be.true;
265 | });
266 |
267 | it('should be able to manually reconnect', async () => {
268 | amqp = new AmqpConnectionManager('amqp://localhost');
269 | await amqp.connect();
270 |
271 | amqp.reconnect();
272 | await once(amqp, 'disconnect');
273 | await once(amqp, 'connect');
274 | });
275 |
276 | it('should throw on manual reconnect after close', async () => {
277 | amqp = new AmqpConnectionManager('amqp://localhost');
278 | await amqp.connect();
279 | await amqp.close();
280 | expect(amqp.reconnect).to.throw();
281 | });
282 |
283 | it('should create and clean up channel wrappers', async function () {
284 | amqp = new AmqpConnectionManager('amqp://localhost');
285 | await amqp.connect();
286 | const channel = amqp.createChannel({ name: 'test-chan' });
287 |
288 | // Channel should register with connection manager
289 | expect(amqp.channelCount, 'registered channels').to.equal(1);
290 | expect(amqp.listeners('connect').length, 'connect listners').to.equal(1);
291 | expect(amqp.listeners('disconnect').length, 'disconnect listners').to.equal(1);
292 |
293 | // Closing the channel should remove all listeners and de-register the channel
294 | await channel.close();
295 |
296 | expect(amqp.channelCount, 'registered channels after close').to.equal(0);
297 | expect(amqp.listeners('connect').length, 'connect listners after close').to.equal(0);
298 | expect(amqp.listeners('disconnect').length, 'disconnect listners after close').to.equal(0);
299 | });
300 |
301 | it('should clean up channels on close', async function () {
302 | amqp = new AmqpConnectionManager('amqp://localhost');
303 | await amqp.connect();
304 | amqp.createChannel({ name: 'test-chan' });
305 |
306 | // Channel should register with connection manager
307 | expect(amqp.channelCount, 'registered channels').to.equal(1);
308 | expect(amqp.listeners('connect').length, 'connect listners').to.equal(1);
309 | expect(amqp.listeners('disconnect').length, 'disconnect listners').to.equal(1);
310 |
311 | // Closing the connection should remove all listeners and de-register the channel
312 | await amqp.close();
313 |
314 | expect(amqp.channelCount, 'registered channels after close').to.equal(0);
315 | expect(amqp.listeners('connect').length, 'connect listners after close').to.equal(0);
316 | expect(amqp.listeners('disconnect').length, 'disconnect listners after close').to.equal(0);
317 | });
318 |
319 | it('should not reconnect after close', async () => {
320 | amqp = new AmqpConnectionManager('amqp://localhost', {
321 | heartbeatIntervalInSeconds: 0.01,
322 | });
323 |
324 | let connectsSeen = 0;
325 | amqp.on('connect', () => connectsSeen++);
326 | await amqp.connect();
327 |
328 | // Close the manager
329 | await amqp?.close();
330 |
331 | // Murder the broker on the first connect
332 | amqplib.kill();
333 |
334 | await promiseTools.delay(50);
335 | expect(connectsSeen).to.equal(1);
336 | });
337 |
338 | it('should detect connection block/unblock', async () => {
339 | amqp = new AmqpConnectionManager('amqp://localhost');
340 |
341 | let blockSeen = 0;
342 | let unblockSeen = 0;
343 |
344 | amqp.on('blocked', () => blockSeen++);
345 |
346 | amqp.on('unblocked', () => unblockSeen++);
347 |
348 | await amqp.connect();
349 | // Close the connection nicely
350 | amqplib.simulateRemoteBlock();
351 | amqplib.simulateRemoteUnblock();
352 |
353 | expect(blockSeen).to.equal(1);
354 | expect(unblockSeen).to.equal(1);
355 | });
356 | });
357 |
--------------------------------------------------------------------------------
/test/ChannelWrapperTest.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 |
3 | import * as amqplib from 'amqplib';
4 | import chai from 'chai';
5 | import chaiAsPromised from 'chai-as-promised';
6 | import chaiJest from 'chai-jest';
7 | import chaiString from 'chai-string';
8 | import * as promiseTools from 'promise-tools';
9 | import ChannelWrapper, { SetupFunc } from '../src/ChannelWrapper';
10 | import * as fixtures from './fixtures';
11 |
12 | chai.use(chaiString);
13 | chai.use(chaiJest);
14 | chai.use(chaiAsPromised);
15 | const { expect } = chai;
16 |
17 | function makeMessage(content: string): amqplib.Message {
18 | return {
19 | content: Buffer.from(content),
20 | fields: {
21 | deliveryTag: 0,
22 | exchange: 'exchange',
23 | redelivered: false,
24 | routingKey: 'routingKey',
25 | },
26 | properties: {
27 | headers: {},
28 | } as any,
29 | };
30 | }
31 |
32 | function getUnderlyingChannel(
33 | channelWrapper: ChannelWrapper
34 | ): fixtures.FakeConfirmChannel | fixtures.FakeChannel {
35 | const channel = (channelWrapper as any)._channel;
36 | if (!channel) {
37 | throw new Error('No underlying channel');
38 | }
39 | return channel;
40 | }
41 |
42 | describe('ChannelWrapper', function () {
43 | let connectionManager: fixtures.FakeAmqpConnectionManager;
44 |
45 | beforeEach(function () {
46 | connectionManager = new fixtures.FakeAmqpConnectionManager() as any;
47 | });
48 |
49 | it('should run all setup functions on connect', async function () {
50 | const setup1 = jest.fn().mockImplementation(() => promiseTools.delay(10));
51 | const setup2 = jest.fn().mockImplementation(() => promiseTools.delay(10));
52 |
53 | const channelWrapper = new ChannelWrapper(connectionManager, { setup: setup1 });
54 |
55 | await channelWrapper.addSetup(setup2);
56 |
57 | expect(setup1).to.have.beenCalledTimes(0);
58 | expect(setup2).to.have.beenCalledTimes(0);
59 |
60 | connectionManager.simulateConnect();
61 |
62 | await channelWrapper.waitForConnect();
63 |
64 | expect(setup1).to.have.beenCalledTimes(1);
65 | expect(setup2).to.have.beenCalledTimes(1);
66 | });
67 |
68 | it('should run all setup functions on reconnect', async function () {
69 | const setup1 = jest.fn().mockImplementation(() => Promise.resolve());
70 | const setup2 = jest.fn().mockImplementation(() => Promise.resolve());
71 |
72 | const channelWrapper = new ChannelWrapper(connectionManager, { setup: setup1 });
73 | await channelWrapper.addSetup(setup2);
74 |
75 | connectionManager.simulateConnect();
76 | await channelWrapper.waitForConnect();
77 |
78 | expect(setup1).to.have.beenCalledTimes(1);
79 | expect(setup2).to.have.beenCalledTimes(1);
80 |
81 | connectionManager.simulateDisconnect();
82 | connectionManager.simulateConnect();
83 | await channelWrapper.waitForConnect();
84 |
85 | expect(setup1).to.have.beenCalledTimes(2);
86 | expect(setup2).to.have.beenCalledTimes(2);
87 | });
88 |
89 | it('should set `this` correctly in a setup function', async function () {
90 | let whatIsThis;
91 |
92 | const channelWrapper = new ChannelWrapper(connectionManager, {
93 | setup() {
94 | // eslint-disable-next-line @typescript-eslint/no-this-alias
95 | whatIsThis = this;
96 | },
97 | });
98 |
99 | connectionManager.simulateConnect();
100 | await channelWrapper.waitForConnect();
101 |
102 | expect(whatIsThis).to.equal(channelWrapper);
103 | });
104 |
105 | it('should emit an error if a setup function throws', async function () {
106 | const setup1 = jest.fn().mockImplementation(() => Promise.resolve());
107 | const setup2 = jest.fn().mockImplementation(() => Promise.reject(new Error('Boom!')));
108 | const errors = [];
109 |
110 | const channelWrapper = new ChannelWrapper(connectionManager, {
111 | setup: setup1,
112 | });
113 |
114 | channelWrapper.on('error', (err) => errors.push(err));
115 |
116 | await channelWrapper.addSetup(setup2);
117 |
118 | connectionManager.simulateConnect();
119 |
120 | await promiseTools.whilst(
121 | () => setup2.mock.calls.length === 0,
122 | () => promiseTools.delay(10)
123 | );
124 |
125 | expect(setup1).to.have.beenCalledTimes(1);
126 | expect(setup2).to.have.beenCalledTimes(1);
127 | expect(errors.length).to.equal(1);
128 | });
129 |
130 | it('should not emit an error if a setup function throws because the channel is closed', async function () {
131 | const setup1 = jest
132 | .fn()
133 | .mockImplementation((channel) => Promise.resolve().then(() => channel.close()));
134 |
135 | const setup2 = jest.fn().mockImplementation(() =>
136 | promiseTools.delay(20).then(function () {
137 | const e = new Error('Channel closed');
138 | e.name = 'IllegalOperationError';
139 | throw e;
140 | })
141 | );
142 |
143 | const errors = [];
144 |
145 | const channelWrapper = new ChannelWrapper(connectionManager, { setup: setup1 });
146 | channelWrapper.on('error', (err) => errors.push(err));
147 |
148 | await channelWrapper.addSetup(setup2);
149 | connectionManager.simulateConnect();
150 |
151 | await promiseTools.delay(50);
152 |
153 | expect(setup1).to.have.beenCalledTimes(1);
154 | expect(setup2).to.have.beenCalledTimes(1);
155 | expect(errors.length).to.equal(0);
156 | });
157 |
158 | it('should return immediately from waitForConnect if we are already connected', function () {
159 | connectionManager.simulateConnect();
160 | const channelWrapper = new ChannelWrapper(connectionManager);
161 | return channelWrapper.waitForConnect().then(() => channelWrapper.waitForConnect());
162 | });
163 |
164 | it('should run setup functions immediately if already connected', async function () {
165 | const setup1 = jest.fn().mockImplementation(() => promiseTools.delay(10));
166 | const setup2 = jest.fn().mockImplementation(() => promiseTools.delay(10));
167 |
168 | connectionManager.simulateConnect();
169 |
170 | const channelWrapper = new ChannelWrapper(connectionManager, {
171 | setup: setup1,
172 | });
173 |
174 | await channelWrapper.waitForConnect();
175 | // Initial setup will be run in background - wait for connect event.
176 | expect(setup1).to.have.beenCalledTimes(1);
177 |
178 | await channelWrapper.addSetup(setup2);
179 |
180 | // Any setups we add after this should get run right away, though.
181 | expect(setup2).to.have.beenCalledTimes(1);
182 | });
183 |
184 | it('should emit errors if setup functions fail to run at connect time', async function () {
185 | const setup = () => Promise.reject(new Error('Bad setup!'));
186 | const setup2 = () => Promise.reject(new Error('Bad setup2!'));
187 | const errorHandler = jest.fn().mockImplementation(function (_err: Error) {});
188 |
189 | const channelWrapper = new ChannelWrapper(connectionManager, { setup });
190 | channelWrapper.on('error', errorHandler);
191 |
192 | connectionManager.simulateConnect();
193 |
194 | await channelWrapper.waitForConnect();
195 |
196 | expect(errorHandler).to.have.beenCalledTimes(1);
197 | expect(lastArgs(errorHandler)?.[0]?.message).to.equal('Bad setup!');
198 |
199 | await expect(channelWrapper.addSetup(setup2)).to.be.rejectedWith('Bad setup2!');
200 |
201 | // Should not be an `error` event here, since we theoretically just handled the error.
202 | expect(errorHandler, 'no second error event').to.have.beenCalledTimes(1);
203 | });
204 |
205 | it('should emit an error if amqplib refuses to create a channel for us', async function () {
206 | const errorHandler = jest.fn().mockImplementation(function (_err: Error) {});
207 |
208 | const channelWrapper = new ChannelWrapper(connectionManager);
209 | channelWrapper.on('error', errorHandler);
210 |
211 | await (channelWrapper as any)._onConnect({
212 | connection: {
213 | createConfirmChannel() {
214 | return Promise.reject(new Error('No channel for you!'));
215 | },
216 | },
217 | });
218 |
219 | expect(errorHandler).to.have.beenCalledTimes(1);
220 | expect(lastArgs(errorHandler)?.[0]?.message).to.equal('No channel for you!');
221 | });
222 |
223 | it('should create plain channel', async function () {
224 | const setup = jest.fn().mockImplementation(() => promiseTools.delay(10));
225 |
226 | connectionManager.simulateConnect();
227 | const channelWrapper = new ChannelWrapper(connectionManager, {
228 | setup,
229 | confirm: false,
230 | });
231 | await channelWrapper.waitForConnect();
232 |
233 | expect(setup).to.have.beenCalledTimes(1);
234 | });
235 |
236 | it('should work if there are no setup functions', async function () {
237 | connectionManager.simulateConnect();
238 | const channelWrapper = new ChannelWrapper(connectionManager);
239 | await channelWrapper.waitForConnect();
240 | // Yay! We didn't blow up!
241 | });
242 |
243 | it('should publish messages to the underlying channel', function () {
244 | connectionManager.simulateConnect();
245 | const channelWrapper = new ChannelWrapper(connectionManager);
246 | return channelWrapper
247 | .waitForConnect()
248 | .then(() =>
249 | channelWrapper.publish('exchange', 'routingKey', 'argleblargle', {
250 | messageId: 'foo',
251 | })
252 | )
253 | .then(function (result) {
254 | expect(result, 'result').to.equal(true);
255 |
256 | // get the underlying channel
257 | const channel = getUnderlyingChannel(channelWrapper);
258 | expect(channel.publish).to.have.beenCalledTimes(1);
259 | expect(lastArgs(channel.publish).slice(0, 4)).to.eql([
260 | 'exchange',
261 | 'routingKey',
262 | Buffer.from('argleblargle'),
263 | { messageId: 'foo' },
264 | ]);
265 |
266 | // Try without options
267 | return channelWrapper.publish('exchange', 'routingKey', 'argleblargle');
268 | })
269 | .then(function (result) {
270 | expect(result, 'second result').to.equal(true);
271 |
272 | // get the underlying channel
273 | const channel = getUnderlyingChannel(channelWrapper);
274 | expect(channel.publish, 'second call to publish').to.have.beenCalledTimes(2);
275 | expect(lastArgs(channel.publish)?.slice(0, 4), 'second args').to.eql([
276 | 'exchange',
277 | 'routingKey',
278 | Buffer.from('argleblargle'),
279 | {},
280 | ]);
281 | expect(channelWrapper.queueLength(), 'queue length').to.equal(0);
282 | });
283 | });
284 |
285 | it('should publish messages to the underlying channel with callbacks', function (done) {
286 | connectionManager.simulateConnect();
287 | const channelWrapper = new ChannelWrapper(connectionManager);
288 | channelWrapper.waitForConnect(function (err) {
289 | if (err) {
290 | return done(err);
291 | }
292 | return channelWrapper.publish(
293 | 'exchange',
294 | 'routingKey',
295 | 'argleblargle',
296 | { messageId: 'foo' },
297 | function (err, result) {
298 | if (err) {
299 | return done(err);
300 | }
301 | try {
302 | expect(result, 'result').to.equal(true);
303 |
304 | // get the underlying channel
305 | const channel = getUnderlyingChannel(channelWrapper);
306 | if (!channel) {
307 | throw new Error('No channel');
308 | }
309 | expect(channel.publish).to.have.beenCalledTimes(1);
310 | expect(lastArgs(channel.publish)?.slice(0, 4), 'publish args').to.eql([
311 | 'exchange',
312 | 'routingKey',
313 | Buffer.from('argleblargle'),
314 | { messageId: 'foo' },
315 | ]);
316 | return done();
317 | } catch (error) {
318 | return done(error);
319 | }
320 | }
321 | );
322 | });
323 | });
324 |
325 | it('should sendToQueue messages to the underlying channel', function () {
326 | connectionManager.simulateConnect();
327 | const channelWrapper = new ChannelWrapper(connectionManager);
328 | return channelWrapper
329 | .waitForConnect()
330 | .then(() => channelWrapper.sendToQueue('queue', 'argleblargle', { messageId: 'foo' }))
331 | .then(function (result) {
332 | expect(result, 'result').to.equal(true);
333 |
334 | // get the underlying channel
335 | const channel = getUnderlyingChannel(channelWrapper);
336 | expect(channel.sendToQueue).to.have.beenCalledTimes(1);
337 | expect(lastArgs(channel.sendToQueue)?.slice(0, 3), 'args').to.eql([
338 | 'queue',
339 | Buffer.from('argleblargle'),
340 | { messageId: 'foo' },
341 | ]);
342 | return expect(channelWrapper.queueLength(), 'queue length').to.equal(0);
343 | });
344 | });
345 |
346 | it('should queue messages for the underlying channel when disconnected', function () {
347 | const channelWrapper = new ChannelWrapper(connectionManager);
348 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'argleblargle', {
349 | messageId: 'foo',
350 | });
351 | const p2 = channelWrapper.sendToQueue('queue', 'argleblargle', {
352 | messageId: 'foo',
353 | });
354 |
355 | expect(channelWrapper.queueLength(), 'queue length').to.equal(2);
356 | connectionManager.simulateConnect();
357 | return channelWrapper
358 | .waitForConnect()
359 | .then(() => Promise.all([p1, p2]))
360 | .then(function () {
361 | // get the underlying channel
362 | const channel = getUnderlyingChannel(channelWrapper);
363 | expect(channel.publish).to.have.beenCalledTimes(1);
364 | expect(channel.sendToQueue).to.have.beenCalledTimes(1);
365 | return expect(
366 | channelWrapper.queueLength(),
367 | 'queue length after sending everything'
368 | ).to.equal(0);
369 | });
370 | });
371 |
372 | it('should queue messages for the underlying channel if channel closes while we are trying to send', async function () {
373 | const channelWrapper = new ChannelWrapper(connectionManager);
374 |
375 | connectionManager.simulateConnect();
376 | await channelWrapper.waitForConnect();
377 | (channelWrapper as any)._channel.publish = function (
378 | _exchange: string,
379 | _routingKey: string,
380 | _encodedMessage: Buffer,
381 | _options: amqplib.Options.Publish,
382 | cb: (err?: Error) => void
383 | ) {
384 | this.close();
385 | return cb(new Error('Channel closed'));
386 | };
387 |
388 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'argleblargle', {
389 | messageId: 'foo',
390 | });
391 |
392 | await promiseTools.delay(10);
393 |
394 | expect((channelWrapper as any)._channel).to.not.exist;
395 |
396 | connectionManager.simulateDisconnect();
397 | connectionManager.simulateConnect();
398 | channelWrapper.waitForConnect();
399 |
400 | await p1;
401 |
402 | // get the underlying channel
403 | const channel = getUnderlyingChannel(channelWrapper);
404 | expect(channel.publish).to.have.beenCalledTimes(1);
405 | return expect(
406 | channelWrapper.queueLength(),
407 | 'queue length after sending everything'
408 | ).to.equal(0);
409 | });
410 |
411 | it('should timeout published message', async function () {
412 | const channelWrapper = new ChannelWrapper(connectionManager);
413 |
414 | const startTime = Date.now();
415 | const error = await channelWrapper
416 | .publish('exchange', 'routingKey', 'argleblargle', {
417 | timeout: 100,
418 | })
419 | .catch((err) => err);
420 | const duration = Date.now() - startTime;
421 | expect(error.message).to.equal('timeout');
422 | expect(duration).to.be.approximately(100, 10);
423 | });
424 |
425 | it('should use default timeout for published messages', async function () {
426 | const channelWrapper = new ChannelWrapper(connectionManager, { publishTimeout: 100 });
427 |
428 | const startTime = Date.now();
429 | const error = await channelWrapper
430 | .publish('exchange', 'routingKey', 'argleblargle')
431 | .catch((err) => err);
432 | const duration = Date.now() - startTime;
433 | expect(error.message).to.equal('timeout');
434 | expect(duration).to.be.approximately(100, 10);
435 | });
436 |
437 | it('should run all setup messages prior to sending any queued messages', function () {
438 | const order: string[] = [];
439 |
440 | const setup: SetupFunc = function (channel: amqplib.ConfirmChannel) {
441 | order.push('setup');
442 |
443 | // Since this should get run before anything gets sent, this is where we need to add listeners to the
444 | // underlying channel.
445 | channel.on('publish', () => order.push('publish'));
446 | channel.on('sendToQueue', () => order.push('sendToQueue'));
447 |
448 | return Promise.resolve();
449 | };
450 |
451 | const channelWrapper = new ChannelWrapper(connectionManager);
452 |
453 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'argleblargle', {
454 | messageId: 'foo',
455 | });
456 | const p2 = channelWrapper.sendToQueue('queue', 'argleblargle', {
457 | messageId: 'foo',
458 | });
459 |
460 | return channelWrapper
461 | .addSetup(setup)
462 | .then(function () {
463 | connectionManager.simulateConnect();
464 | return channelWrapper.waitForConnect();
465 | })
466 | .then(() => Promise.all([p1, p2]))
467 | .then(() => expect(order).to.eql(['setup', 'publish', 'sendToQueue']));
468 | });
469 |
470 | it('should remove setup messages', async () => {
471 | const setup = jest.fn().mockImplementation(() => Promise.resolve());
472 |
473 | const channelWrapper = new ChannelWrapper(connectionManager);
474 | await channelWrapper.addSetup(setup);
475 | await channelWrapper.removeSetup(setup);
476 |
477 | connectionManager.simulateConnect();
478 | await channelWrapper.waitForConnect();
479 |
480 | expect(setup).to.have.not.beenCalled;
481 | });
482 |
483 | it('should fail silently when removing a setup that was not added', async () => {
484 | const channelWrapper = new ChannelWrapper(connectionManager);
485 | await channelWrapper.removeSetup(() => undefined);
486 | });
487 |
488 | it('should run teardown when removing a setup if we are connected', async function () {
489 | const setup = jest.fn().mockImplementation(() => Promise.resolve());
490 | const teardown = jest.fn().mockImplementation(() => Promise.resolve());
491 |
492 | const channelWrapper = new ChannelWrapper(connectionManager);
493 | await channelWrapper.addSetup(setup);
494 |
495 | expect(teardown, 'pre-teardown count').to.have.not.beenCalled;
496 |
497 | connectionManager.simulateConnect();
498 | await channelWrapper.waitForConnect();
499 |
500 | await channelWrapper.removeSetup(setup, teardown);
501 | expect(teardown).to.have.beenCalledTimes(1);
502 | });
503 |
504 | it('should proxy acks and nacks to the underlying channel', function () {
505 | connectionManager.simulateConnect();
506 | const channelWrapper = new ChannelWrapper(connectionManager);
507 | return channelWrapper.waitForConnect().then(function () {
508 | // get the underlying channel
509 | const channel = getUnderlyingChannel(channelWrapper);
510 |
511 | const message = makeMessage('a');
512 | channelWrapper.ack(message, true);
513 | expect(channel.ack).to.have.beenCalledTimes(1);
514 | expect(channel.ack).to.have.beenCalledWith(message, true);
515 |
516 | channelWrapper.ack(message);
517 | expect(channel.ack).to.have.beenCalledTimes(2);
518 | expect(channel.ack).to.have.beenCalledWith(message, undefined);
519 |
520 | channelWrapper.ackAll();
521 | expect(channel.ackAll).to.have.beenCalledTimes(1);
522 |
523 | channelWrapper.nack(message, false, true);
524 | expect(channel.nack).to.have.beenCalledTimes(1);
525 | expect(channel.nack).to.have.beenCalledWith(message, false, true);
526 |
527 | channelWrapper.nackAll(true);
528 | expect(channel.nackAll).to.have.beenCalledTimes(1);
529 | expect(channel.nackAll).to.have.beenCalledWith(true);
530 | });
531 | });
532 |
533 | it("should proxy acks and nacks to the underlying channel, even if we aren't done setting up", async () => {
534 | const channelWrapper = new ChannelWrapper(connectionManager);
535 |
536 | const a = makeMessage('a');
537 | const b = makeMessage('b');
538 |
539 | channelWrapper.addSetup(function () {
540 | channelWrapper.ack(a);
541 | channelWrapper.nack(b);
542 | return Promise.resolve();
543 | });
544 |
545 | connectionManager.simulateConnect();
546 |
547 | await channelWrapper.waitForConnect();
548 | const channel = getUnderlyingChannel(channelWrapper);
549 |
550 | expect(channel.ack).to.have.beenCalledTimes(1);
551 | expect(channel.ack).to.have.beenCalledWith(a, undefined);
552 | expect(channel.nack).to.have.beenCalledTimes(1);
553 | expect(channel.nack).to.have.beenCalledWith(b, undefined, undefined);
554 | });
555 |
556 | it('should ignore acks and nacks if we are disconnected', function () {
557 | const channelWrapper = new ChannelWrapper(connectionManager);
558 | channelWrapper.ack(makeMessage('a'), true);
559 | return channelWrapper.nack(makeMessage('c'), false, true);
560 | });
561 |
562 | it('should proxy assertQueue, checkQueue, bindQueue, assertExchange, checkExchange to the underlying channel', function () {
563 | connectionManager.simulateConnect();
564 | const channelWrapper = new ChannelWrapper(connectionManager);
565 | return channelWrapper.waitForConnect().then(function () {
566 | // get the underlying channel
567 | const channel = getUnderlyingChannel(channelWrapper);
568 |
569 | channelWrapper.assertQueue('dog');
570 | expect(channel.assertQueue).to.have.beenCalledTimes(1);
571 | expect(channel.assertQueue).to.have.beenCalledWith('dog', undefined);
572 |
573 | channelWrapper.checkQueue('cat');
574 | expect(channel.checkQueue).to.have.beenCalledTimes(1);
575 | expect(channel.checkQueue).to.have.beenCalledWith('cat');
576 |
577 | channelWrapper.bindQueue('dog', 'bone', '.*');
578 | expect(channel.bindQueue).to.have.beenCalledTimes(1);
579 | expect(channel.bindQueue).to.have.beenCalledWith('dog', 'bone', '.*', undefined);
580 |
581 | channelWrapper.assertExchange('bone', 'topic');
582 | expect(channel.assertExchange).to.have.beenCalledTimes(1);
583 | expect(channel.assertExchange).to.have.beenCalledWith('bone', 'topic', undefined);
584 |
585 | channelWrapper.checkExchange('fish');
586 | expect(channel.checkExchange).to.have.beenCalledTimes(1);
587 | expect(channel.checkExchange).to.have.beenCalledWith('fish');
588 |
589 | });
590 | });
591 |
592 | it('should proxy assertQueue, assertExchange, bindQueue and unbindQueue to the underlying channel', function () {
593 | connectionManager.simulateConnect();
594 | const channelWrapper = new ChannelWrapper(connectionManager);
595 | return channelWrapper.waitForConnect().then(function () {
596 | // get the underlying channel
597 | const channel = getUnderlyingChannel(channelWrapper);
598 |
599 | channelWrapper.assertQueue('dog');
600 | expect(channel.assertQueue).to.have.beenCalledTimes(1);
601 | expect(channel.assertQueue).to.have.beenCalledWith('dog', undefined);
602 |
603 | channelWrapper.assertExchange('bone', 'topic');
604 | expect(channel.assertExchange).to.have.beenCalledTimes(1);
605 | expect(channel.assertExchange).to.have.beenCalledWith('bone', 'topic', undefined);
606 |
607 | channelWrapper.bindQueue('dog', 'bone', 'legs');
608 | expect(channel.bindQueue).to.have.beenCalledTimes(1);
609 | expect(channel.bindQueue).to.have.beenCalledWith('dog', 'bone', 'legs', undefined);
610 |
611 | channelWrapper.unbindQueue('dog', 'bone', 'legs');
612 | expect(channel.unbindQueue).to.have.beenCalledTimes(1);
613 | expect(channel.unbindQueue).to.have.beenCalledWith('dog', 'bone', 'legs', undefined);
614 | });
615 | });
616 |
617 | it(`should proxy assertQueue, bindQueue, assertExchange to the underlying channel, even if we aren't done setting up`, async () => {
618 | const channelWrapper = new ChannelWrapper(connectionManager);
619 |
620 | channelWrapper.addSetup(function () {
621 | channelWrapper.assertQueue('dog');
622 | channelWrapper.bindQueue('dog', 'bone', '.*');
623 | channelWrapper.assertExchange('bone', 'topic');
624 | return Promise.resolve();
625 | });
626 |
627 | connectionManager.simulateConnect();
628 |
629 | await channelWrapper.waitForConnect();
630 | const channel = getUnderlyingChannel(channelWrapper);
631 | expect(channel.assertQueue).to.have.beenCalledTimes(1);
632 | expect(channel.assertQueue).to.have.beenCalledWith('dog', undefined);
633 |
634 | expect(channel.bindQueue).to.have.beenCalledTimes(1);
635 | expect(channel.bindQueue).to.have.beenCalledWith('dog', 'bone', '.*', undefined);
636 |
637 | expect(channel.assertExchange).to.have.beenCalledTimes(1);
638 | expect(channel.assertExchange).to.have.beenCalledWith('bone', 'topic', undefined);
639 | });
640 |
641 | it('should ignore assertQueue, bindQueue, assertExchange if we are disconnected', function () {
642 | const channelWrapper = new ChannelWrapper(connectionManager);
643 | channelWrapper.assertQueue('dog', { durable: true });
644 | channelWrapper.bindQueue('dog', 'bone', '.*');
645 | channelWrapper.assertExchange('bone', 'topic');
646 | });
647 |
648 | it('should proxy bindExchange, unbindExchange and deleteExchange to the underlying channel', function () {
649 | connectionManager.simulateConnect();
650 | const channelWrapper = new ChannelWrapper(connectionManager);
651 | return channelWrapper.waitForConnect().then(function () {
652 | // get the underlying channel
653 | const channel = getUnderlyingChannel(channelWrapper);
654 |
655 | channelWrapper.bindExchange('paris', 'london', '*');
656 | expect(channel.bindExchange).to.have.beenCalledTimes(1);
657 | expect(channel.bindExchange).to.have.beenCalledWith('paris', 'london', '*', undefined);
658 |
659 | channelWrapper.unbindExchange('paris', 'london', '*');
660 | expect(channel.unbindExchange).to.have.beenCalledTimes(1);
661 | expect(channel.unbindExchange).to.have.beenCalledWith(
662 | 'paris',
663 | 'london',
664 | '*',
665 | undefined
666 | );
667 |
668 | channelWrapper.deleteExchange('chicago');
669 | expect(channel.deleteExchange).to.have.beenCalledTimes(1);
670 | expect(channel.deleteExchange).to.have.beenCalledWith('chicago', undefined);
671 | });
672 | });
673 |
674 | // Not much to test here - just make sure we don't throw any exceptions or anything weird. :)
675 |
676 | it('clean up when closed', function () {
677 | let closeEvents = 0;
678 |
679 | connectionManager.simulateConnect();
680 | const channelWrapper = new ChannelWrapper(connectionManager);
681 | channelWrapper.on('close', () => closeEvents++);
682 | return channelWrapper.waitForConnect().then(function () {
683 | const channel = getUnderlyingChannel(channelWrapper);
684 | return channelWrapper.close().then(function () {
685 | // Should close the channel.
686 | expect(channel.close).to.have.beenCalledTimes(1);
687 |
688 | // Channel should let the connectionManager know it's going away.
689 | return expect(closeEvents).to.equal(1);
690 | });
691 | });
692 | });
693 |
694 | it('clean up when closed when not connected', function () {
695 | let closeEvents = 0;
696 |
697 | return Promise.resolve()
698 | .then(function () {
699 | const channelWrapper = new ChannelWrapper(connectionManager);
700 | channelWrapper.on('close', () => closeEvents++);
701 | return channelWrapper.close();
702 | })
703 | .then(() =>
704 | // Channel should let the connectionManager know it's going away.
705 | expect(closeEvents).to.equal(1)
706 | );
707 | });
708 |
709 | it('reject outstanding messages when closed', function () {
710 | const channelWrapper = new ChannelWrapper(connectionManager);
711 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'argleblargle', {
712 | messageId: 'foo',
713 | });
714 | return Promise.all([channelWrapper.close(), expect(p1).to.be.rejected]);
715 | });
716 |
717 | it('should encode JSON messages', function () {
718 | connectionManager.simulateConnect();
719 | const channelWrapper = new ChannelWrapper(connectionManager, {
720 | json: true,
721 | });
722 | return channelWrapper
723 | .waitForConnect()
724 | .then(() =>
725 | channelWrapper.publish('exchange', 'routingKey', {
726 | message: 'woo',
727 | })
728 | )
729 | .then(function () {
730 | // get the underlying channel
731 | const channel = getUnderlyingChannel(channelWrapper);
732 | expect(channel.publish).to.have.beenCalledTimes(1);
733 | const content = lastArgs(channel.publish)?.[2];
734 | return expect(content.write, 'content should be a buffer').to.exist;
735 | });
736 | });
737 |
738 | it('should reject messages when JSON encoding fails', function () {
739 | const badJsonMesage: { x: any } = { x: 7 };
740 | badJsonMesage.x = badJsonMesage;
741 |
742 | connectionManager.simulateConnect();
743 | const channelWrapper = new ChannelWrapper(connectionManager, {
744 | json: true,
745 | });
746 | return channelWrapper.waitForConnect().then(function () {
747 | const p1 = channelWrapper.publish('exchange', 'routingKey', badJsonMesage);
748 | return expect(p1).to.be.rejected;
749 | });
750 | });
751 |
752 | it('should reject messages if they get rejected by the broker', async function () {
753 | connectionManager.simulateConnect();
754 | const channelWrapper = new ChannelWrapper(connectionManager, {
755 | setup(channel: amqplib.ConfirmChannel) {
756 | channel.publish = (
757 | _exchange: string,
758 | _routingKey: string,
759 | _encodedMessage: Buffer,
760 | _options: amqplib.Options.Publish,
761 | cb: (err?: Error) => void
762 | ) => {
763 | cb(new Error('no publish'));
764 | return true;
765 | };
766 | channel.sendToQueue = (_a: any, _b: any, _c: any, cb: (err?: Error) => void) => {
767 | cb(new Error('no send'));
768 | return true;
769 | };
770 | return Promise.resolve();
771 | },
772 | });
773 |
774 | await channelWrapper.waitForConnect();
775 |
776 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'content');
777 | const p2 = channelWrapper.sendToQueue('queue', 'content');
778 |
779 | await expect(p1).to.be.rejectedWith('no publish');
780 | await expect(p2).to.be.rejectedWith('no send');
781 | });
782 |
783 | it('should reject correct message if broker rejects out of order', async function () {
784 | connectionManager.simulateConnect();
785 |
786 | const callbacks: {
787 | message: Buffer;
788 | cb: (err?: Error) => void;
789 | }[] = [];
790 |
791 | const channelWrapper = new ChannelWrapper(connectionManager, {
792 | setup(channel: amqplib.ConfirmChannel) {
793 | channel.publish = (
794 | _exchange: string,
795 | _routingKey: string,
796 | message: Buffer,
797 | _options: amqplib.Options.Publish,
798 | cb: (err?: Error) => void
799 | ) => {
800 | callbacks.push({ message, cb });
801 | return true;
802 | };
803 | return Promise.resolve();
804 | },
805 | });
806 |
807 | await channelWrapper.waitForConnect();
808 |
809 | channelWrapper.publish('exchange', 'routingKey', 'content1');
810 | const p2 = channelWrapper.publish('exchange', 'routingKey', 'content2');
811 | await promiseTools.delay(10);
812 | expect(callbacks).to.have.length(2);
813 |
814 | // Nack the second message.
815 | callbacks.find((c) => c.message.toString() === 'content2')?.cb(new Error('boom'));
816 | await expect(p2).to.be.rejectedWith('boom');
817 |
818 | // Simulate a disconnect and reconnect.
819 | connectionManager.simulateDisconnect();
820 | await promiseTools.delay(10);
821 | callbacks.find((c) => c.message.toString() === 'content1')?.cb(new Error('disconnected'));
822 | connectionManager.simulateConnect();
823 | await promiseTools.delay(10);
824 | expect(callbacks).to.have.length(3);
825 |
826 | // Make sure the first message is resent.
827 | const resent = callbacks[callbacks.length - 1];
828 | expect(resent.message.toString()).to.equal('content1');
829 | });
830 |
831 | it('should keep sending messages, even if we disconnect in the middle of sending', async function () {
832 | const callbacks: ((err: Error | undefined) => void)[] = [];
833 |
834 | connectionManager.simulateConnect();
835 | const channelWrapper = new ChannelWrapper(connectionManager, {
836 | setup(channel: amqplib.ConfirmChannel) {
837 | channel.publish = function (
838 | _exchange: string,
839 | _routingKey: string,
840 | _message: Buffer,
841 | _options: amqplib.Options.Publish,
842 | cb: (err?: Error) => void
843 | ) {
844 | callbacks.push(cb);
845 | return true;
846 | };
847 |
848 | return Promise.resolve();
849 | },
850 | });
851 |
852 | await channelWrapper.waitForConnect();
853 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'content');
854 |
855 | // Disconnect, and generate an error for the in-flight message.
856 | await promiseTools.delay(10);
857 | connectionManager.simulateDisconnect();
858 | expect(callbacks).to.have.length(1);
859 | callbacks[0](new Error('disconnected'));
860 |
861 | // Reconnect. Should resend the message.
862 | await promiseTools.delay(10);
863 | connectionManager.simulateConnect();
864 | await promiseTools.delay(10);
865 | expect(callbacks).to.have.length(2);
866 | callbacks[1](undefined);
867 | await p1;
868 | });
869 |
870 | it('should handle getting a confirm out-of-order with a disconnect', async function () {
871 | const callbacks: ((err: Error | undefined) => void)[] = [];
872 |
873 | connectionManager.simulateConnect();
874 | const channelWrapper = new ChannelWrapper(connectionManager, {
875 | setup(channel: amqplib.ConfirmChannel) {
876 | channel.publish = function (
877 | _exchange: string,
878 | _routingKey: string,
879 | _message: Buffer,
880 | _options: amqplib.Options.Publish,
881 | cb: (err?: Error) => void
882 | ) {
883 | callbacks.push(cb);
884 | return true;
885 | };
886 |
887 | return Promise.resolve();
888 | },
889 | });
890 |
891 | await channelWrapper.waitForConnect();
892 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'content');
893 |
894 | // Disconnect.
895 | await promiseTools.delay(10);
896 | connectionManager.simulateDisconnect();
897 | expect(callbacks).to.have.length(1);
898 |
899 | // Message succeeds after disconnect.
900 | callbacks[0](undefined);
901 |
902 | // Reconnect. Should resend the message.
903 | await promiseTools.delay(10);
904 | connectionManager.simulateConnect();
905 | await promiseTools.delay(10);
906 | expect(callbacks).to.have.length(1);
907 | await p1;
908 | });
909 |
910 | it('should handle getting a confirm out-of-order with a disconnect and reconnect', async function () {
911 | const callbacks: ((err: Error | undefined) => void)[] = [];
912 |
913 | connectionManager.simulateConnect();
914 | const channelWrapper = new ChannelWrapper(connectionManager, {
915 | setup(channel: amqplib.ConfirmChannel) {
916 | channel.publish = function (
917 | _exchange: string,
918 | _routingKey: string,
919 | _message: Buffer,
920 | _options: amqplib.Options.Publish,
921 | cb: (err?: Error) => void
922 | ) {
923 | callbacks.push(cb);
924 | return true;
925 | };
926 |
927 | return Promise.resolve();
928 | },
929 | });
930 |
931 | await channelWrapper.waitForConnect();
932 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'content');
933 |
934 | // Disconnect.
935 | await promiseTools.delay(10);
936 | connectionManager.simulateDisconnect();
937 | expect(callbacks).to.have.length(1);
938 |
939 | // Reconnect.
940 | await promiseTools.delay(10);
941 | connectionManager.simulateConnect();
942 | await promiseTools.delay(10);
943 |
944 | // Message from first connection succeeds after disconnect/reconnect.
945 | expect(callbacks).to.have.length(1);
946 | callbacks[0](undefined);
947 | await p1;
948 | });
949 |
950 | it('should emit an error, we disconnect during publish with code 502 (AMQP Frame Syntax Error)', function () {
951 | connectionManager.simulateConnect();
952 | const err = new Error('AMQP Frame Syntax Error');
953 | (err as any).code = 502;
954 | const channelWrapper = new ChannelWrapper(connectionManager, {
955 | setup(channel: amqplib.ConfirmChannel) {
956 | channel.publish = function (
957 | _exchange: string,
958 | _routingKey: string,
959 | _message: Buffer,
960 | _options: amqplib.Options.Publish,
961 | cb: (err?: Error) => void
962 | ) {
963 | connectionManager.simulateRemoteCloseEx(err);
964 | cb();
965 | return true;
966 | };
967 | return Promise.resolve();
968 | },
969 | });
970 |
971 | return channelWrapper
972 | .waitForConnect()
973 | .then(() => channelWrapper.publish('exchange', 'routingKey', 'content'))
974 | .then(function () {})
975 | .catch((e) => {
976 | expect(e).to.equal(err);
977 | });
978 | });
979 |
980 | it('should retry, we disconnect during publish with code 320 (AMQP Connection Forced Error)', async function () {
981 | const callbacks: ((err: Error | undefined) => void)[] = [];
982 |
983 | connectionManager.simulateConnect();
984 | const err = new Error('AMQP Frame Syntax Error');
985 | (err as any).code = 320;
986 | const channelWrapper = new ChannelWrapper(connectionManager, {
987 | setup(channel: amqplib.ConfirmChannel) {
988 | channel.publish = function (
989 | _exchange: string,
990 | _routingKey: string,
991 | _message: Buffer,
992 | _options: amqplib.Options.Publish,
993 | cb: (err?: Error) => void
994 | ) {
995 | callbacks.push(cb);
996 | return true;
997 | };
998 | return Promise.resolve();
999 | },
1000 | });
1001 |
1002 | await channelWrapper.waitForConnect();
1003 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'content');
1004 | await promiseTools.delay(10);
1005 | expect(callbacks).to.have.length(1);
1006 |
1007 | // Simulate disconnect during publish.
1008 | connectionManager.simulateRemoteCloseEx(err);
1009 | callbacks[0](new Error('disconnected'));
1010 |
1011 | // Reconnect.
1012 | connectionManager.simulateConnect();
1013 | await promiseTools.delay(10);
1014 |
1015 | // Message should be republished.
1016 | expect(callbacks).to.have.length(2);
1017 | callbacks[1](undefined);
1018 | await p1;
1019 | });
1020 |
1021 | it('should publish queued messages to the underlying channel without waiting for confirms', async function () {
1022 | connectionManager.simulateConnect();
1023 | const channelWrapper = new ChannelWrapper(connectionManager, {
1024 | setup(channel: amqplib.ConfirmChannel) {
1025 | channel.publish = jest.fn().mockImplementation(() => true);
1026 | return Promise.resolve();
1027 | },
1028 | });
1029 |
1030 | await channelWrapper.waitForConnect();
1031 | const p1 = channelWrapper.publish('exchange', 'routingKey', 'msg:1');
1032 | const p2 = channelWrapper.publish('exchange', 'routingKey', 'msg:2');
1033 | await promiseTools.delay(10);
1034 |
1035 | const channel = getUnderlyingChannel(channelWrapper);
1036 | expect(channel.publish).to.have.beenCalledTimes(2);
1037 | expect(p1).to.not.be.fulfilled;
1038 | expect(p2).to.not.be.fulfilled;
1039 | });
1040 |
1041 | it('should stop publishing messages to the queue when the queue is full', async function () {
1042 | const queue: (() => void)[] = [];
1043 | let innerChannel: amqplib.Channel = {} as any;
1044 |
1045 | connectionManager.simulateConnect();
1046 | const channelWrapper = new ChannelWrapper(connectionManager, {
1047 | async setup(channel: amqplib.ConfirmChannel) {
1048 | innerChannel = channel;
1049 | channel.publish = jest
1050 | .fn()
1051 | .mockImplementation((_exchage, _routingKey, content, _options, callback) => {
1052 | channel.emit('publish', content);
1053 | queue.push(() => callback(null));
1054 | return queue.length < 2;
1055 | });
1056 | },
1057 | });
1058 |
1059 | await channelWrapper.waitForConnect();
1060 |
1061 | channelWrapper.publish('exchange', 'routingKey', 'msg:1');
1062 | channelWrapper.publish('exchange', 'routingKey', 'msg:2');
1063 | channelWrapper.publish('exchange', 'routingKey', 'msg:3');
1064 | await promiseTools.delay(10);
1065 |
1066 | // Only two messages should have been published to the underlying queue.
1067 | expect(queue.length).to.equal(2);
1068 |
1069 | // Simulate queue draining.
1070 | queue.pop()!();
1071 | innerChannel.emit('drain');
1072 |
1073 | await promiseTools.delay(10);
1074 |
1075 | // Final message should have been published to the underlying queue.
1076 | expect(queue.length).to.equal(2);
1077 | });
1078 |
1079 | it('should consume messages', async function () {
1080 | let onMessage: any = null;
1081 |
1082 | connectionManager.simulateConnect();
1083 | const channelWrapper = new ChannelWrapper(connectionManager, {
1084 | async setup(channel: amqplib.ConfirmChannel) {
1085 | channel.consume = jest.fn().mockImplementation((_queue, onMsg, _options) => {
1086 | onMessage = onMsg;
1087 | return Promise.resolve({ consumerTag: 'abc' });
1088 | });
1089 | },
1090 | });
1091 | await channelWrapper.waitForConnect();
1092 |
1093 | const messages: any[] = [];
1094 | await channelWrapper.consume(
1095 | 'queue',
1096 | (msg) => {
1097 | messages.push(msg);
1098 | },
1099 | { noAck: true }
1100 | );
1101 |
1102 | onMessage(1);
1103 | onMessage(2);
1104 | onMessage(3);
1105 | expect(messages).to.deep.equal([1, 2, 3]);
1106 | });
1107 |
1108 | it('should reconnect consumer on consumer cancellation', async function () {
1109 | let onMessage: any = null;
1110 | let consumerTag = 0;
1111 |
1112 | connectionManager.simulateConnect();
1113 | const channelWrapper = new ChannelWrapper(connectionManager, {
1114 | async setup(channel: amqplib.ConfirmChannel) {
1115 | channel.consume = jest.fn().mockImplementation((_queue, onMsg, _options) => {
1116 | onMessage = onMsg;
1117 | return Promise.resolve({ consumerTag: `${consumerTag++}` });
1118 | });
1119 | },
1120 | });
1121 | await channelWrapper.waitForConnect();
1122 |
1123 | const messages: any[] = [];
1124 | await channelWrapper.consume('queue', (msg) => {
1125 | messages.push(msg);
1126 | });
1127 |
1128 | onMessage(1);
1129 | onMessage(null); // simulate consumer cancel
1130 | onMessage(2);
1131 | onMessage(null); // simulate second cancel
1132 | onMessage(3);
1133 |
1134 | expect(messages).to.deep.equal([1, 2, 3]);
1135 | expect(consumerTag).to.equal(3);
1136 | });
1137 |
1138 | it('should reconnect consumers on channel error', async function () {
1139 | let onQueue1: any = null;
1140 | let onQueue2: any = null;
1141 | let consumerTag = 0;
1142 |
1143 | // Define a prefetch function here, because it will otherwise be
1144 | // unique for each new channel
1145 | const prefetchFn = jest
1146 | .fn()
1147 | .mockImplementation((_prefetch: number, _isGlobal: boolean) => {});
1148 |
1149 | connectionManager.simulateConnect();
1150 | const channelWrapper = new ChannelWrapper(connectionManager, {
1151 | async setup(channel: amqplib.ConfirmChannel) {
1152 | channel.prefetch = prefetchFn;
1153 | channel.consume = jest.fn().mockImplementation((queue, onMsg, _options) => {
1154 | if (queue === 'queue1') {
1155 | onQueue1 = onMsg;
1156 | } else {
1157 | onQueue2 = onMsg;
1158 | }
1159 | return Promise.resolve({ consumerTag: `${consumerTag++}` });
1160 | });
1161 | },
1162 | });
1163 | await channelWrapper.waitForConnect();
1164 |
1165 | const queue1: any[] = [];
1166 | await channelWrapper.consume(
1167 | 'queue1',
1168 | (msg) => {
1169 | queue1.push(msg);
1170 | },
1171 | { noAck: true, prefetch: 10 }
1172 | );
1173 |
1174 | const queue2: any[] = [];
1175 | await channelWrapper.consume('queue2', (msg) => {
1176 | queue2.push(msg);
1177 | });
1178 |
1179 | onQueue1(1);
1180 | onQueue2(1);
1181 |
1182 | connectionManager.simulateDisconnect();
1183 | connectionManager.simulateConnect();
1184 | await channelWrapper.waitForConnect();
1185 |
1186 | onQueue1(2);
1187 | onQueue2(2);
1188 |
1189 | expect(queue1).to.deep.equal([1, 2]);
1190 | expect(queue2).to.deep.equal([1, 2]);
1191 | expect(consumerTag).to.equal(4);
1192 | expect(prefetchFn).to.have.beenCalledTimes(2);
1193 | expect(prefetchFn).to.have.beenNthCalledWith(1, 10, false);
1194 | expect(prefetchFn).to.have.beenNthCalledWith(2, 10, false);
1195 | });
1196 |
1197 | it('should be able to cancel all consumers', async function () {
1198 | let onQueue1: any = null;
1199 | let onQueue2: any = null;
1200 | let consumerTag = 0;
1201 | const canceledTags: number[] = [];
1202 |
1203 | connectionManager.simulateConnect();
1204 | const channelWrapper = new ChannelWrapper(connectionManager, {
1205 | async setup(channel: amqplib.ConfirmChannel) {
1206 | channel.consume = jest.fn().mockImplementation((queue, onMsg, _options) => {
1207 | if (queue === 'queue1') {
1208 | onQueue1 = onMsg;
1209 | } else {
1210 | onQueue2 = onMsg;
1211 | }
1212 | return Promise.resolve({ consumerTag: `${consumerTag++}` });
1213 | });
1214 | channel.cancel = jest.fn().mockImplementation((consumerTag) => {
1215 | canceledTags.push(consumerTag);
1216 | if (consumerTag === '0') {
1217 | onQueue1(null);
1218 | } else if (consumerTag === '1') {
1219 | onQueue2(null);
1220 | }
1221 | return Promise.resolve();
1222 | });
1223 | },
1224 | });
1225 | await channelWrapper.waitForConnect();
1226 |
1227 | const queue1: any[] = [];
1228 | await channelWrapper.consume('queue1', (msg) => {
1229 | queue1.push(msg);
1230 | });
1231 |
1232 | const queue2: any[] = [];
1233 | await channelWrapper.consume('queue2', (msg) => {
1234 | queue2.push(msg);
1235 | });
1236 |
1237 | onQueue1(1);
1238 | onQueue2(1);
1239 |
1240 | await channelWrapper.cancelAll();
1241 |
1242 | // Consumers shouldn't be resumed after reconnect when canceled
1243 | connectionManager.simulateDisconnect();
1244 | connectionManager.simulateConnect();
1245 | await channelWrapper.waitForConnect();
1246 |
1247 | expect(queue1).to.deep.equal([1]);
1248 | expect(queue2).to.deep.equal([1]);
1249 | expect(consumerTag).to.equal(2);
1250 | expect(canceledTags).to.deep.equal(['0', '1']);
1251 | });
1252 |
1253 | it('should be able to cancel specific consumers', async function () {
1254 | let onQueue1: any = null;
1255 | let onQueue2: any = null;
1256 | const canceledTags: number[] = [];
1257 |
1258 | connectionManager.simulateConnect();
1259 | const channelWrapper = new ChannelWrapper(connectionManager, {
1260 | async setup(channel: amqplib.ConfirmChannel) {
1261 | channel.consume = jest.fn().mockImplementation((queue, onMsg, _options) => {
1262 | if (queue === 'queue1') {
1263 | onQueue1 = onMsg;
1264 | } else {
1265 | onQueue2 = onMsg;
1266 | }
1267 | return Promise.resolve({
1268 | consumerTag: _options.consumerTag,
1269 | });
1270 | });
1271 | channel.cancel = jest.fn().mockImplementation((consumerTag) => {
1272 | canceledTags.push(consumerTag);
1273 | if (consumerTag === '0') {
1274 | onQueue1(null);
1275 | } else if (consumerTag === '1') {
1276 | onQueue2(null);
1277 | }
1278 | return Promise.resolve();
1279 | });
1280 | },
1281 | });
1282 | await channelWrapper.waitForConnect();
1283 |
1284 | const queue1: any[] = [];
1285 | const { consumerTag: consumerTag1 } = await channelWrapper.consume(
1286 | 'queue1',
1287 | (msg) => {
1288 | queue1.push(msg);
1289 | },
1290 | { consumerTag: '1' }
1291 | );
1292 |
1293 | const queue2: any[] = [];
1294 | const { consumerTag: consumerTag2 } = await channelWrapper.consume(
1295 | 'queue2',
1296 | (msg) => {
1297 | queue2.push(msg);
1298 | },
1299 | { consumerTag: '2' }
1300 | );
1301 |
1302 | onQueue1(1);
1303 | onQueue2(1);
1304 |
1305 | await channelWrapper.cancel(consumerTag1);
1306 | await channelWrapper.cancel(consumerTag2);
1307 |
1308 | // Consumers shouldn't be resumed after reconnect when canceled
1309 | connectionManager.simulateDisconnect();
1310 | connectionManager.simulateConnect();
1311 | await channelWrapper.waitForConnect();
1312 |
1313 | expect(queue1).to.deep.equal([1]);
1314 | expect(queue2).to.deep.equal([1]);
1315 | expect(canceledTags).to.deep.equal(['1', '2']);
1316 | });
1317 |
1318 | it('should not register same consumer twice', async function () {
1319 | const setup = jest.fn().mockImplementation(() => promiseTools.delay(10));
1320 |
1321 | const channelWrapper = new ChannelWrapper(connectionManager, { setup });
1322 | connectionManager.simulateConnect();
1323 |
1324 | await channelWrapper.consume('queue', () => {});
1325 |
1326 | await channelWrapper.waitForConnect();
1327 |
1328 | const channel = getUnderlyingChannel(channelWrapper);
1329 | expect(channel.consume).to.have.beenCalledTimes(1);
1330 | });
1331 | });
1332 |
1333 | /** Returns the arguments of the most recent call to this mock. */
1334 | function lastArgs(mock: jest.Mock): Y | undefined {
1335 | if (mock.mock.calls.length === 0) {
1336 | return undefined;
1337 | }
1338 | return mock.mock.calls[mock.mock.calls.length - 1];
1339 | }
1340 |
--------------------------------------------------------------------------------
/test/fixtures.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 |
4 | import { Connection, Message, Options, Replies } from 'amqplib';
5 | import { EventEmitter, once } from 'events';
6 | import { IAmqpConnectionManager } from '../src/AmqpConnectionManager';
7 | import ChannelWrapper, { CreateChannelOpts } from '../src/ChannelWrapper';
8 |
9 | export class FakeAmqp {
10 | public connection: Connection | undefined;
11 | public url: string | undefined;
12 | public failConnections = false;
13 | public deadServers: string[] = [];
14 | public connect: (url: string) => Promise = async () => {
15 | throw new Error('Not setup');
16 | };
17 |
18 | constructor() {
19 | this.reset();
20 | }
21 |
22 | kill() {
23 | const err = new Error('Died in a fire');
24 | this.connection?.emit('error', err);
25 | this.connection?.emit('close', err);
26 | }
27 |
28 | simulateRemoteClose() {
29 | this.connection?.emit('close', new Error('Connection closed'));
30 | }
31 |
32 | simulateRemoteBlock() {
33 | this.connection?.emit('blocked', new Error('Connection blocked'));
34 | }
35 |
36 | simulateRemoteUnblock() {
37 | this.connection?.emit('unblocked');
38 | }
39 |
40 | reset() {
41 | this.connection = undefined;
42 | this.url = undefined;
43 | this.failConnections = false;
44 | this.deadServers = [];
45 | this.connect = jest.fn().mockImplementation((url) => {
46 | if (this.failConnections) {
47 | return Promise.reject(new Error('No'));
48 | }
49 |
50 | let allowConnection = true;
51 | this.deadServers.forEach((deadUrl) => {
52 | if (url.startsWith(deadUrl)) {
53 | allowConnection = false;
54 | }
55 | });
56 | if (!allowConnection) {
57 | return Promise.reject(new Error(`Dead server ${url}`));
58 | }
59 |
60 | const connection = (this.connection = new exports.FakeConnection(url));
61 | return Promise.resolve(connection);
62 | });
63 | }
64 | }
65 |
66 | export class FakeChannel extends EventEmitter {
67 | publish = jest
68 | .fn()
69 | .mockImplementation(
70 | (
71 | _exchange: string,
72 | _routingKey: string,
73 | content: Buffer,
74 | _options?: Options.Publish
75 | ): boolean => {
76 | this.emit('publish', content);
77 | return true;
78 | }
79 | );
80 |
81 | sendToQueue = jest
82 | .fn()
83 | .mockImplementation(
84 | (_queue: string, content: Buffer, _options?: Options.Publish): boolean => {
85 | this.emit('sendToQueue', content);
86 | return true;
87 | }
88 | );
89 |
90 | ack = jest.fn().mockImplementation(function (_message: Message, _allUpTo?: boolean): void {});
91 |
92 | ackAll = jest.fn().mockImplementation(function (): void {});
93 |
94 | nack = jest
95 | .fn()
96 | .mockImplementation(function (
97 | _message: Message,
98 | _allUpTo?: boolean,
99 | _requeue?: boolean
100 | ): void {});
101 |
102 | nackAll = jest.fn().mockImplementation(function (_requeue?: boolean): void {});
103 |
104 | assertQueue = jest
105 | .fn()
106 | .mockImplementation(async function (
107 | queue: string,
108 | _options?: Options.AssertQueue
109 | ): Promise {
110 | return {
111 | queue,
112 | messageCount: 0,
113 | consumerCount: 0,
114 | };
115 | });
116 |
117 | checkQueue = jest
118 | .fn()
119 | .mockImplementation(async function (_queue: string): Promise {
120 | return {};
121 | });
122 |
123 | bindQueue = jest
124 | .fn()
125 | .mockImplementation(async function (
126 | _queue: string,
127 | _source: string,
128 | _pattern: string,
129 | _args?: any
130 | ): Promise {
131 | return {};
132 | });
133 |
134 | unbindQueue = jest
135 | .fn()
136 | .mockImplementation(async function (
137 | _queue: string,
138 | _source: string,
139 | _pattern: string,
140 | _args?: any
141 | ): Promise {
142 | return {};
143 | });
144 |
145 | assertExchange = jest
146 | .fn()
147 | .mockImplementation(async function (
148 | exchange: string,
149 | _type: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string,
150 | _options?: Options.AssertExchange
151 | ): Promise {
152 | return { exchange };
153 | });
154 |
155 | bindExchange = jest
156 | .fn()
157 | .mockImplementation(async function (
158 | _destination: string,
159 | _source: string,
160 | _pattern: string,
161 | _args?: any
162 | ): Promise {
163 | return {};
164 | });
165 |
166 | checkExchange = jest
167 | .fn()
168 | .mockImplementation(async function (_exchange: string): Promise {
169 | return {};
170 | });
171 |
172 | deleteExchange = jest
173 | .fn()
174 | .mockImplementation(async function (
175 | _exchange: string,
176 | _options?: Options.DeleteExchange
177 | ): Promise {
178 | return {};
179 | });
180 |
181 | unbindExchange = jest
182 | .fn()
183 | .mockImplementation(async function (
184 | _destination: string,
185 | _source: string,
186 | _pattern: string,
187 | _args?: any
188 | ): Promise {
189 | return {};
190 | });
191 |
192 | close = jest.fn().mockImplementation(async (): Promise => {
193 | this.emit('close');
194 | });
195 |
196 | consume = jest.fn().mockImplementation(async (): Promise => {
197 | return { consumerTag: 'abc' };
198 | });
199 |
200 | prefetch = jest.fn().mockImplementation((_prefetch: number, _isGlobal: boolean): void => {});
201 | }
202 |
203 | export class FakeConfirmChannel extends FakeChannel {
204 | publish = jest
205 | .fn()
206 | .mockImplementation(
207 | (
208 | _exchange: string,
209 | _routingKey: string,
210 | content: Buffer,
211 | _options?: Options.Publish,
212 | callback?: (err: any, ok: Replies.Empty) => void
213 | ): boolean => {
214 | this.emit('publish', content);
215 | callback?.(null, {});
216 | return true;
217 | }
218 | );
219 |
220 | sendToQueue = jest
221 | .fn()
222 | .mockImplementation(
223 | (
224 | _queue: string,
225 | content: Buffer,
226 | _options?: Options.Publish,
227 | callback?: (err: any, ok: Replies.Empty) => void
228 | ): boolean => {
229 | this.emit('sendToQueue', content);
230 | callback?.(null, {});
231 | return true;
232 | }
233 | );
234 | }
235 |
236 | export class FakeConnection extends EventEmitter {
237 | url: string;
238 | _closed = false;
239 |
240 | constructor(url: string) {
241 | super();
242 | this.url = url;
243 | this._closed = false;
244 | }
245 |
246 | createChannel() {
247 | return Promise.resolve(new exports.FakeChannel());
248 | }
249 |
250 | createConfirmChannel() {
251 | return Promise.resolve(new exports.FakeConfirmChannel());
252 | }
253 |
254 | close() {
255 | this._closed = true;
256 | return Promise.resolve();
257 | }
258 | }
259 |
260 | export class FakeAmqpConnectionManager extends EventEmitter implements IAmqpConnectionManager {
261 | connected: boolean;
262 | private _connection: FakeConnection | undefined;
263 |
264 | heartbeatIntervalInSeconds = 5;
265 | reconnectTimeInSeconds = 10;
266 |
267 | constructor() {
268 | super();
269 | this.connected = false;
270 | }
271 |
272 | get connection() {
273 | return this._connection as any as Connection | undefined;
274 | }
275 |
276 | get channelCount(): number {
277 | return 0;
278 | }
279 |
280 | async connect(): Promise {
281 | await Promise.all([once(this, 'connect'), this.simulateConnect()]);
282 | }
283 |
284 | reconnect(): void {
285 | this.simulateDisconnect();
286 | this.simulateConnect();
287 | }
288 |
289 | isConnected() {
290 | return this.connected;
291 | }
292 |
293 | createChannel(options?: CreateChannelOpts): ChannelWrapper {
294 | return new ChannelWrapper(this, options);
295 | }
296 |
297 | simulateConnect() {
298 | const url = 'amqp://localhost';
299 | this._connection = new exports.FakeConnection(url);
300 | this.connected = true;
301 | this.emit('connect', {
302 | connection: this.connection,
303 | url,
304 | });
305 | }
306 |
307 | simulateRemoteCloseEx(err: Error) {
308 | this.emit('disconnect', { err });
309 | this.emit('close', err);
310 | }
311 |
312 | simulateDisconnect() {
313 | this._connection = undefined;
314 | this.connected = false;
315 | this.emit('disconnect', {
316 | err: new Error('Boom!'),
317 | });
318 | }
319 |
320 | async close() {}
321 | }
322 |
--------------------------------------------------------------------------------
/test/importTest.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import amqp, { AmqpConnectionManagerClass as AmqpConnectionManager } from '../src';
3 |
4 | describe('import test', function () {
5 | it('should let you import as default (#51)', function () {
6 | expect(amqp).to.exist;
7 | expect(amqp.connect).to.exist;
8 | });
9 |
10 | it('should let you import class', function () {
11 | new AmqpConnectionManager('url');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/test/integrationTest.ts:
--------------------------------------------------------------------------------
1 | import { Channel, ConfirmChannel, ConsumeMessage } from 'amqplib';
2 | import chai from 'chai';
3 | import chaiJest from 'chai-jest';
4 | import { once } from 'events';
5 | import { defer, timeout } from 'promise-tools';
6 | import amqp, { AmqpConnectionManagerClass as AmqpConnectionManager } from '../src';
7 | import { IAmqpConnectionManager } from '../src/AmqpConnectionManager';
8 |
9 | chai.use(chaiJest);
10 |
11 | const { expect } = chai;
12 |
13 | /**
14 | * Tests in this file assume you have a RabbitMQ instance running on localhost.
15 | * You can start one with:
16 | *
17 | * docker-compose up -d
18 | *
19 | */
20 | describe('Integration tests', () => {
21 | let connection: IAmqpConnectionManager;
22 |
23 | afterEach(async () => {
24 | await connection?.close();
25 | });
26 |
27 | it('should connect to the broker', async () => {
28 | // Create a new connection manager
29 | connection = amqp.connect(['amqp://localhost']);
30 | await timeout(once(connection, 'connect'), 3000);
31 | });
32 |
33 | it('should connect to the broker with a username and password', async () => {
34 | // Create a new connection manager
35 | connection = amqp.connect(['amqp://guest:guest@localhost:5672']);
36 | await timeout(once(connection, 'connect'), 3000);
37 | });
38 |
39 | it('should connect to the broker with a string', async () => {
40 | // Create a new connection manager
41 | connection = amqp.connect('amqp://guest:guest@localhost:5672');
42 | await timeout(once(connection, 'connect'), 3000);
43 | });
44 |
45 | it('should connect to the broker with a amqp.Connect object', async () => {
46 | // Create a new connection manager
47 | connection = amqp.connect({
48 | protocol: 'amqp',
49 | hostname: 'localhost',
50 | port: 5672,
51 | vhost: '/',
52 | });
53 | await timeout(once(connection, 'connect'), 3000);
54 | });
55 |
56 | it('should connect to the broker with an url/options object', async () => {
57 | // Create a new connection manager
58 | connection = amqp.connect({
59 | url: 'amqp://guest:guest@localhost:5672',
60 | });
61 | await timeout(once(connection, 'connect'), 3000);
62 | });
63 |
64 | it('should connect to the broker with a string with options', async () => {
65 | // Create a new connection manager
66 | connection = amqp.connect(
67 | 'amqp://guest:guest@localhost:5672/%2F?heartbeat=10&channelMax=100'
68 | );
69 | await timeout(once(connection, 'connect'), 3000);
70 | });
71 |
72 | // This test might cause jest to complain about leaked resources due to the bug described and fixed by:
73 | // https://github.com/squaremo/amqp.node/pull/584
74 | it('should throw on awaited connect with wrong password', async () => {
75 | connection = new AmqpConnectionManager('amqp://guest:wrong@localhost');
76 | let err;
77 | try {
78 | await connection.connect();
79 | } catch (error: any) {
80 | err = error;
81 | }
82 | expect(err.message).to.contain('ACCESS-REFUSED');
83 | });
84 |
85 | it('send and receive messages', async () => {
86 | const queueName = 'testQueue1';
87 | const content = `hello world - ${Date.now()}`;
88 |
89 | // Create a new connection manager
90 | connection = amqp.connect(['amqp://localhost']);
91 |
92 | // Ask the connection manager for a ChannelWrapper. Specify a setup function to
93 | // run every time we reconnect to the broker.
94 | const sendChannel = connection.createChannel({
95 | setup: async (channel: ConfirmChannel) => {
96 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
97 | },
98 | });
99 |
100 | const rxPromise = defer();
101 |
102 | const receiveWrapper = connection.createChannel({
103 | setup: async (channel: ConfirmChannel) => {
104 | // `channel` here is a regular amqplib `ConfirmChannel`.
105 | // Note that `this` here is the channelWrapper instance.
106 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
107 | await channel.consume(
108 | queueName,
109 | (message) => {
110 | if (!message) {
111 | // Ignore.
112 | } else if (message.content.toString() === content) {
113 | rxPromise.resolve(message);
114 | } else {
115 | console.log(
116 | `Received message ${message?.content.toString()} !== ${content}`
117 | );
118 | }
119 | },
120 | {
121 | noAck: true,
122 | }
123 | );
124 | },
125 | });
126 |
127 | await timeout(once(connection, 'connect'), 3000);
128 |
129 | await sendChannel.sendToQueue(queueName, content);
130 |
131 | const result = await timeout(rxPromise.promise, 3000);
132 | expect(result.content.toString()).to.equal(content);
133 |
134 | await sendChannel.close();
135 | await receiveWrapper.close();
136 | });
137 |
138 | it('send and receive messages with plain channel', async () => {
139 | const queueName = 'testQueue2';
140 | const content = `hello world - ${Date.now()}`;
141 |
142 | connection = new AmqpConnectionManager('amqp://localhost');
143 | const sendChannel = connection.createChannel({
144 | confirm: false,
145 | setup: async (channel: Channel) => {
146 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
147 | },
148 | });
149 |
150 | const receiveChannel = connection.createChannel({
151 | confirm: false,
152 | setup: async (channel: Channel) => {
153 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
154 | },
155 | });
156 |
157 | await connection.connect();
158 |
159 | const rxPromise = defer();
160 | await receiveChannel.consume(queueName, (message) => {
161 | rxPromise.resolve(message);
162 | });
163 |
164 | await sendChannel.sendToQueue(queueName, content);
165 |
166 | const result = await timeout(rxPromise.promise, 3000);
167 | expect(result.content.toString()).to.equal(content);
168 |
169 | await sendChannel.close();
170 | await receiveChannel.close();
171 | });
172 |
173 | it('RPC', async () => {
174 | const queueName = 'testQueueRpc';
175 |
176 | // Create a new connection manager
177 | connection = amqp.connect(['amqp://localhost']);
178 |
179 | let rpcClientQueueName = '';
180 |
181 | const result = defer();
182 |
183 | // Ask the connection manager for a ChannelWrapper. Specify a setup function to
184 | // run every time we reconnect to the broker.
185 | const rpcClient = connection.createChannel({
186 | setup: async (channel: ConfirmChannel) => {
187 | const qok = await channel.assertQueue('', { exclusive: true });
188 | rpcClientQueueName = qok.queue;
189 |
190 | await channel.consume(
191 | rpcClientQueueName,
192 | (message) => {
193 | result.resolve(message?.content.toString());
194 | },
195 | { noAck: true }
196 | );
197 | },
198 | });
199 |
200 | const rpcServer = connection.createChannel({
201 | setup: async (channel: ConfirmChannel) => {
202 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
203 | await channel.prefetch(1);
204 | await channel.consume(
205 | queueName,
206 | (message) => {
207 | if (message) {
208 | channel.sendToQueue(message.properties.replyTo, Buffer.from('world'), {
209 | correlationId: message.properties.correlationId,
210 | });
211 | }
212 | },
213 | { noAck: true }
214 | );
215 | },
216 | });
217 |
218 | await timeout(once(connection, 'connect'), 3000);
219 | await timeout(rpcClient.waitForConnect(), 3000);
220 | await timeout(rpcServer.waitForConnect(), 3000);
221 |
222 | // Send message from client to server.
223 | await rpcClient.sendToQueue(queueName, 'hello', {
224 | correlationId: 'test',
225 | replyTo: rpcClientQueueName,
226 | messageId: 'asdkasldk',
227 | });
228 |
229 | const reply = await result.promise;
230 | expect(reply).to.equal('world');
231 |
232 | await rpcClient.close();
233 | await rpcServer.close();
234 | });
235 |
236 | it('direct-reply-to', async () => {
237 | // See https://www.rabbitmq.com/direct-reply-to.html
238 | const rpcClientQueueName = 'amq.rabbitmq.reply-to';
239 | const queueName = 'testQueueRpc';
240 |
241 | // Create a new connection manager
242 | connection = amqp.connect(['amqp://localhost']);
243 |
244 | const result = defer();
245 |
246 | connection.on('disconnect', ({ err }) => {
247 | if (err) {
248 | console.log(err);
249 | }
250 | });
251 |
252 | // Ask the connection manager for a ChannelWrapper. Specify a setup function to
253 | // run every time we reconnect to the broker.
254 | const rpcClient = connection.createChannel({
255 | setup: async (channel: ConfirmChannel) => {
256 | await channel.consume(
257 | rpcClientQueueName,
258 | (message) => {
259 | result.resolve(message?.content.toString());
260 | },
261 | { noAck: true }
262 | );
263 | },
264 | });
265 |
266 | const rpcServer = connection.createChannel({
267 | setup: async (channel: ConfirmChannel) => {
268 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
269 | await channel.prefetch(1);
270 | await channel.consume(
271 | queueName,
272 | (message) => {
273 | if (message) {
274 | channel.sendToQueue(message.properties.replyTo, Buffer.from('world'), {
275 | correlationId: message.properties.correlationId,
276 | });
277 | }
278 | },
279 | { noAck: true }
280 | );
281 | },
282 | });
283 |
284 | await timeout(once(connection, 'connect'), 3000);
285 | await timeout(rpcServer.waitForConnect(), 3000);
286 | await timeout(rpcClient.waitForConnect(), 3000);
287 |
288 | // Send message from client to server.
289 | await rpcClient.sendToQueue(queueName, 'hello', {
290 | correlationId: 'test',
291 | replyTo: rpcClientQueueName,
292 | messageId: 'asdkasldk',
293 | });
294 |
295 | const reply = await result.promise;
296 | expect(reply).to.equal('world');
297 |
298 | await rpcClient.close();
299 | await rpcServer.close();
300 | });
301 |
302 | it('should reconnect consumer after queue deletion', async function () {
303 | const queueName = 'testQueue';
304 |
305 | connection = new AmqpConnectionManager('amqp://localhost', { reconnectTimeInSeconds: 0.5 });
306 | const channelWrapper = connection.createChannel({
307 | confirm: true,
308 | setup: async (channel: Channel) => {
309 | await channel.assertQueue(queueName, { durable: false, autoDelete: true });
310 | },
311 | });
312 |
313 | const result = defer();
314 | await channelWrapper.consume(queueName, (msg) => {
315 | result.resolve(msg.content.toString());
316 | });
317 |
318 | await Promise.all([connection.connect(), once(channelWrapper, 'connect')]);
319 |
320 | // The deleted queue should cause a reconnect
321 | await channelWrapper.deleteQueue(queueName);
322 |
323 | // Await all setup functions to run before sending a message
324 | await once(channelWrapper, 'connect');
325 | await channelWrapper.sendToQueue(queueName, 'hello');
326 |
327 | const content = await result.promise;
328 | expect(content).to.equal('hello');
329 | });
330 | });
331 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "typeRoots": ["../node_modules/@types", "../@types"]
6 | },
7 | "include": ["./**/*"]
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "sourceMap": false,
6 | "module": "commonjs",
7 | "outDir": "./dist/cjs"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es2018",
5 | "module": "es6",
6 | "lib": ["es2018"],
7 | "allowJs": false,
8 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
9 | "declaration": false,
10 | "sourceMap": true,
11 | "outDir": "./dist/esm",
12 | "stripInternal": true,
13 |
14 | /* Strict Type-Checking Options */
15 | "strict": true /* Enable all strict type-checking options. */,
16 | // "strictNullChecks": true, /* Enable strict null checks. */
17 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
18 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
19 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
20 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
21 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
22 |
23 | /* Additional Checks */
24 | "forceConsistentCasingInFileNames": true,
25 | "noUnusedLocals": true /* Report errors on unused locals. */,
26 | "noUnusedParameters": true /* Report errors on unused parameters. */,
27 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
28 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
29 |
30 | /* Module Resolution Options */
31 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
32 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
33 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
34 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
35 | "typeRoots": [
36 | /* List of folders to include type definitions from. */ "./node_modules/@types",
37 | "./@types"
38 | ],
39 | // "typeRoots": [], /* List of folders to include type definitions from. */
40 | // "types": [], /* Type declaration files to be included in compilation. */
41 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
42 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
43 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
44 |
45 | /* Source Map Options */
46 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
47 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
48 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
49 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
50 |
51 | /* Experimental Options */
52 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
53 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
54 | },
55 | "include": ["./src/**/*"],
56 | "compileOnSave": true
57 | }
58 |
--------------------------------------------------------------------------------
/tsconfig.types.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "outDir": "./dist/types"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------