├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── LICENSE-MIT ├── Makefile ├── README.md ├── bin ├── amqp-rabbitmq-0.9.1.json └── generate-defs.js ├── callback_api.js ├── channel_api.js ├── examples ├── direct_reply_to_client.js ├── direct_reply_to_server.js ├── headers.js ├── receive_generator.js ├── send_generators.js ├── ssl.js ├── stream_queues │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── receive_stream.js │ └── send_stream.js ├── tutorials │ ├── README.md │ ├── callback_api │ │ ├── emit_log.js │ │ ├── emit_log_direct.js │ │ ├── emit_log_topic.js │ │ ├── new_task.js │ │ ├── receive.js │ │ ├── receive_logs.js │ │ ├── receive_logs_direct.js │ │ ├── receive_logs_topic.js │ │ ├── rpc_client.js │ │ ├── rpc_server.js │ │ ├── send.js │ │ └── worker.js │ ├── emit_log.js │ ├── emit_log_direct.js │ ├── emit_log_topic.js │ ├── new_task.js │ ├── package.json │ ├── receive.js │ ├── receive_logs.js │ ├── receive_logs_direct.js │ ├── receive_logs_topic.js │ ├── rpc_client.js │ ├── rpc_server.js │ ├── send.js │ └── worker.js └── waitForConfirms.js ├── lib ├── api_args.js ├── bitset.js ├── callback_model.js ├── channel.js ├── channel_model.js ├── codec.js ├── connect.js ├── connection.js ├── credentials.js ├── defs.js ├── error.js ├── format.js ├── frame.js ├── heartbeat.js └── mux.js ├── package-lock.json ├── package.json └── test ├── bitset.js ├── callback_api.js ├── channel.js ├── channel_api.js ├── codec.js ├── connect.js ├── connection.js ├── data.js ├── frame.js ├── mux.js └── util.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report / Support request 3 | about: Report a bug or ask for help 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before reporting a bug or requesting support please... 11 | 12 | 1. Read the troubleshooting guide 13 | 14 | - https://amqp-node.github.io/amqplib/#troubleshooting 15 | 16 | 2. Search for existing open and closed issues 17 | 18 | - https://github.com/amqp-node/amqplib/issues?q=is%3Aissue+TYPE+YOUR+KEYWORDS+HERE 19 | 20 | 3. Ensure you are familiar with the amqplib Channel API and RabbitMQ basics 21 | 22 | - https://amqp-node.github.io/amqplib/channel_api.html 23 | - https://www.cloudamqp.com/blog/part1-rabbitmq-for-beginners-what-is-rabbitmq.html 24 | - https://www.rabbitmq.com/tutorials/amqp-concepts.html 25 | 26 | If the above does not help, please provide as much of the following information as is relevant... 27 | 28 | - A clear and concise description of the problem 29 | - RabbitMQ version 30 | - amqplib version 31 | - Node.js version 32 | - The simplest possible code snippet that demonstrates the problem 33 | - A stack trace 34 | 35 | Please format code snippets and/or stack traces using GitHub Markdown 36 | 37 | - https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks). 38 | 39 | Thank you 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | services: 11 | rabbitmq: 12 | image: rabbitmq:3-alpine 13 | ports: 14 | - 5672:5672 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: "npm" 29 | 30 | - run: npm ci 31 | 32 | - run: | 33 | n=0 34 | while : 35 | do 36 | sleep 5 37 | echo 'HELO\n\n\n\n' | nc localhost 5672 | grep AMQP 38 | [[ $? = 0 ]] && break || ((n++)) 39 | (( n >= 5 )) && break 40 | done 41 | 42 | - run: echo 'HELO\n\n\n\n' | nc localhost 5672 | grep AMQP 43 | 44 | - run: make test 45 | 46 | publish: 47 | needs: build 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v3 51 | - uses: actions/setup-node@v4 52 | with: 53 | node-version: '20.x' 54 | cache: "npm" 55 | registry-url: https://registry.npmjs.org/ 56 | - run: npm ci 57 | - run: npm publish --dry-run 58 | env: 59 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | services: 16 | rabbitmq: 17 | image: rabbitmq:3.12-alpine 18 | ports: 19 | - 5672:5672 20 | 21 | strategy: 22 | matrix: 23 | node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x] 24 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: "npm" 33 | 34 | # Install all prerequisites 35 | - run: npm ci 36 | 37 | # Ensure RabbitMQ is available before continuing 38 | - run: | 39 | n=0 40 | while : 41 | do 42 | sleep 5 43 | echo 'HELO\n\n\n\n' | nc localhost 5672 | grep AMQP 44 | [[ $? = 0 ]] && break || ((n++)) 45 | (( n >= 5 )) && break 46 | done 47 | 48 | - run: echo 'HELO\n\n\n\n' | nc localhost 5672 | grep AMQP 49 | 50 | # Run the tests 51 | - run: make test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | scratch 3 | node_modules/ 4 | etc/ 5 | coverage/ 6 | /.idea/ 7 | .nyc_output/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *~ 2 | scratch 3 | # do not ignore lib/defs.js, we need that 4 | # node_modules is ignored anyway 5 | .travis.yml 6 | etc/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log for amqplib 2 | 3 | ## v0.10.8 4 | - Updated README 5 | 6 | ## v0.10.7 7 | - Remove bitsyntax dependency - See https://github.com/amqp-node/amqplib/pull/785. Thanks @ikenfin 8 | - Stop checking if frame max is exceeded when parsing frames - See https://github.com/amqp-node/amqplib/pull/784. Thanks @ohroy 9 | 10 | ## v0.10.6 11 | - Replace references to the old squaremo/amqp.name repo with ones to amqp-node/amqplib 12 | - Use a frame_max of 131072 by default for RabbitMQ 4.1.0 compatibility 13 | 14 | ## Changes in v0.10.5 15 | 16 | git log v0.10.4..v0.10.5 17 | 18 | - Removed readable stream - See https://github.com/amqp-node/amqplib/issues/729 19 | - Added support for unsigned integers - See https://github.com/amqp-node/amqplib/pull/773 20 | - Committed protocol definitions - See https://github.com/amqp-node/amqplib/commit/0a87ee480311633cff41e43350a90cb3c1221506 21 | 22 | ## Changes in v0.10.4 23 | 24 | - Improve stream example as per https://github.com/amqp-node/amqplib/issues/722 25 | - Added support for RabbitMQ's connection update-secret operation. See https://github.com/amqp-node/amqplib/issues/755 26 | 27 | ## Changes in v0.10.3 28 | 29 | git log v0.10.2..v0.10.3 30 | 31 | - Use @acuminous/bitsyntax fork. See https://github.com/amqp-node/amqplib/issues/453 32 | 33 | ## Changes in v0.10.2 34 | 35 | git log v0.10.1..v0.10.2 36 | 37 | - Use Buffer.allocUnsafe when sending messages to improve performance ([PR 38 | 695](https://github.com/amqp-node/amqplib/pull/695), thank you 39 | @chkimes and @Uzlopak) 40 | 41 | ## Changes in v0.10.1 42 | 43 | git log v0.10.0..v0.10.1 44 | 45 | * Allow servername to be specified via socket options as discussed in 46 | [issue 697](https://github.com/squaremo/amqp.node/issues/697) 47 | 48 | ## Changes in v0.10.0 49 | 50 | git log v0.9.1..v0.10.0 51 | 52 | * Use Native promises ([PR 53 | 689](https://github.com/amqp-node/amqplib/pull/689), thank you 54 | @mohd-akram and @kibertoad) 55 | 56 | ## Changes in v0.9.1 57 | 58 | git log v0.9.0..v0.9.1 59 | 60 | * Assorted readme changes 61 | * Use Array.prototype.push.apply instead of concat in Mux ([PR 62 | 658](https://github.com/squaremo/amqp.node/pull/658), thank you 63 | @Uzlopak and @kibertoad) 64 | * Use Map instead of Object for BaseChannel.consumers ([PR 65 | 660](https://github.com/squaremo/amqp.node/pull/660), thank you 66 | @Uzlopak) 67 | * Delete consumer callback after cancellation to free memory ([PR 68 | 659](https://github.com/squaremo/amqp.node/pull/659), thank you 69 | @Uzlopak and @kibertoad) 70 | 71 | 72 | ## Changes in v0.9.0 73 | 74 | git log v0.8.0..v0.9.0 75 | 76 | * Update mocha and replace the deprecated istanbul with nyc ([PR 77 | 681](https://github.com/squaremo/amqp.node/pull/681) 78 | * Update url-parse ([PR 79 | 675](https://github.com/squaremo/amqp.node/pull/675), thank you 80 | @suhail-n and @kibertoad) 81 | * fix: done called twice on invalid options ([PR 82 | 667](https://github.com/squaremo/amqp.node/pull/667), thank you 83 | @luddd3 and @kibertoad) 84 | * Close connection to server on connect errors ([PR 85 | 647](https://github.com/squaremo/amqp.node/pull/647), thank you 86 | @luddd3 and @kibertoad) 87 | * Modernise channel_model.js ([PR 88 | 635](https://github.com/squaremo/amqp.node/pull/635), thank you 89 | @kibertoad and @jimmywarting) 90 | * Bring package-lock.json up to date ([PR 91 | 653](https://github.com/squaremo/amqp.node/pull/653) 92 | * Update url-parse ([PR 93 | 652](https://github.com/squaremo/amqp.node/pull/652), thank you 94 | @giorgioatanasov and @buffolander) 95 | * Modernise channel_model.js ([PR 96 | 651](https://github.com/squaremo/amqp.node/pull/651), thank you 97 | for the review @kibertoad) 98 | * Modernise bitset.js ([PR 99 | 634](https://github.com/squaremo/amqp.node/pull/634), thank you 100 | @kibertoad and @jimmywarting) 101 | * :warning: Drop CI for node versions below 10 ([PR 102 | 631](https://github.com/squaremo/amqp.node/pull/631), thank you 103 | for the review @kibertoad) 104 | * Replace safe-buffer dependency with native buffers ([PR 105 | 628](https://github.com/squaremo/amqp.node/pull/628), thank you 106 | @kibertoad and @jimmywarting) 107 | 108 | ## Changes in v0.8.0 109 | 110 | git log v0.7.1..v0.8.0 111 | 112 | * :warning: Support for NodeJS prior to v10 is dropped :warning: ([PR 113 | 615](https://github.com/squaremo/amqp.node/pull/615), thank you 114 | @xamgore and everyone who helped there) 115 | * Use hostname as TLS servername, to help with using servers behind 116 | load balancers ([PR 117 | 567](https://github.com/squaremo/amqp.node/pull/567), thanks to 118 | @carlhoerberg and commenters) 119 | 120 | ## Changes in v0.7.1 121 | 122 | git log v0.7.0..v0.7.1 123 | 124 | * Update url-parse (and others) ([PR 125 | 607](https://github.com/squaremo/amqp.node/pull/545), thanks 126 | @ThomasGawlitza) 127 | 128 | ## Changes in v0.7.0 129 | 130 | git log v0.6.0..v0.7.0 131 | 132 | * Extend support to Node.js v15 133 | * Fix use of stream.write in tests 134 | 135 | ## Changes in v0.6.0 136 | 137 | git log v0.5.6..v0.6.0 138 | 139 | * Extend support to Node.js v14 140 | 141 | ## Changes in v0.5.6 142 | 143 | git log v0.5.5..v0.5.6 144 | 145 | * Increase size of encoding space for message headers, to fit e.g., 146 | JWT ([PR 545](https://github.com/squaremo/amqp.node/pull/545)); 147 | thanks @twatson83 148 | * Switch to a non-deprecated UUID module ([PR 149 | 528](https://github.com/squaremo/amqp.node/pull/528)); thanks to 150 | @StrayBird-ATSH 151 | * Fix a bug in multiplexing that caused an assertion to fail ([PR 152 | 503](https://github.com/squaremo/amqp.node/pull/503)); thanks 153 | @johanneswuerbach 154 | 155 | ## Changes in v0.5.5 156 | 157 | git log v0.5.3..v0.5.5 158 | 159 | **NB** this includes a minor but possibly breaking change: after [PR 160 | 498](https://github.com/squaremo/amqp.node/pull/498), all confirmation 161 | promises still unresolved will be rejected when their associated 162 | channel is closed. 163 | 164 | * Generate defs in `npm prepare` rather than `npm prepublish` so that 165 | e.g., amqplib can be installed via git ([part of PR 166 | 498](https://github.com/squaremo/amqp.node/pull/498)) 167 | * Reject all pending confirmations when the channel is closed ([PR 168 | 498](https://github.com/squaremo/amqp.node/pull/498)); thanks 169 | @johanneswuerbach 170 | * Update supported NodeJS versions in package.json ([PR 171 | 525](https://github.com/squaremo/amqp.node/pull/525)); thanks 172 | @tingwai 173 | 174 | ## (Deprecated v0.5.4) 175 | 176 | This release was mistakenly published without the generated file 177 | `./defs.js`. It has been deprecated in favour of v0.5.5. 178 | 179 | ## Changes in v0.5.3 180 | 181 | git log v0.5.2..v0.5.3 182 | 183 | * Bump bitsyntax to remove some `Buffer` misuse deprecation notices 184 | ([PR 480])(https://github.com/squaremo/amqp.node/pull/480) 185 | * Test on node 11.1 186 | ([PR 473])(https://github.com/squaremo/amqp.node/pull/464); thanks 187 | @kibertoad 188 | * Updated various dependencies 189 | * Support queue mode during assertQueue 190 | ([PR 464])(https://github.com/squaremo/amqp.node/pull/464); thanks 191 | @JoeTheFkingFrypan 192 | * Expose serverProperties in the connection object 193 | ([PR 452])(https://github.com/squaremo/amqp.node/pull/452); thanks 194 | @jfromaniello 195 | * Test on Node 10 196 | ([PR 454])(https://github.com/squaremo/amqp.node/pull/454); thanks 197 | @kibertoad 198 | * Support amqplain credentials 199 | ([PR 451])(https://github.com/squaremo/amqp.node/pull/451); thanks 200 | @jfromaniello 201 | * Decorate channel errors with methodId and classId 202 | ([PR 447])(https://github.com/squaremo/amqp.node/pull/447); thanks 203 | @MitMaro 204 | * Resolve issues caused by Node 10 `readable` changes 205 | ([PR 442])(https://github.com/squaremo/amqp.node/pull/442) 206 | * Bump uglify to 2.6.x and node to 9.1 due to nodejs/node#16781. 207 | ([PR 439])(https://github.com/squaremo/amqp.node/pull/439) 208 | * Updated README with more modern Buffer syntax 209 | ([PR 438](https://github.com/squaremo/amqp.node/pull/438); thanks 210 | @ravshansbox 211 | * Support overflow option to assert queue 212 | ([PR 436])(https://github.com/squaremo/amqp.node/pull/436); thanks 213 | to @honestserpent 214 | * Replace instances of keyword `await` 215 | ([PR 396])(https://github.com/squaremo/amqp.node/pull/396), 216 | as discussed in 217 | [issue 235](https://github.com/squaremo/amqp.node/issues/235) 218 | * Use 3rd party url for better decoding of username/password 219 | ([PR 395])(https://github.com/squaremo/amqp.node/pull/395), 220 | as discussed in 221 | [issue 385](https://github.com/squaremo/amqp.node/issues/385)) 222 | 223 | ## Changes in v0.5.2 224 | 225 | git log v0.5.1..v0.5.2 226 | 227 | * Increase encoding buffer to accommodate large header values 228 | ([PR 367](https://github.com/squaremo/amqp.node/pull/367)) 229 | * Bring code up to date with new Buffer interface 230 | ([PR 350](https://github.com/squaremo/amqp.node/pull/350)) 231 | * Fix dangling connection problem 232 | ([PR 340](https://github.com/squaremo/amqp.node/pull/340)) 233 | * Clear up URL credentials parsing 234 | ([PR 330](https://github.com/squaremo/amqp.node/pull/330)) 235 | * Allow connection params to be suppied in object 236 | ([PR 304](https://github.com/squaremo/amqp.node/pull/304)) 237 | * Support explicit numeric types in field tables (e.g., headers) 238 | ([PR 389](https://github.com/squaremo/amqp.node/pull/389), from a 239 | suggestion in 240 | [issue 358](https://github.com/squaremo/amqp.node/issues/358)) 241 | 242 | Thank you to all contributors, of PRs, issues and comments. 243 | 244 | ## Changes in v0.5.1 245 | 246 | git log v0.5.0..v0.5.1 247 | 248 | * Fix mistake in closeBecause 249 | ([PR 298](https://github.com/squaremo/amqp.node/pull/298); thanks 250 | to @lholznagel and others who reported the issue, and to @nfantone 251 | for the rapid fix) 252 | 253 | ## Changes in v0.5.0 254 | 255 | git log v0.4.2..v0.5.0 256 | 257 | * Port to use bluebird rather than when.js 258 | ([PR 295](https://github.com/squaremo/amqp.node/pull/295); thanks 259 | to @nfantone, and special mention to @myndzi for #158) 260 | * Fixed a problem with using `channel.get` in the callback model 261 | ([PR 283](https://github.com/squaremo/amqp.node/pull/283); good 262 | catch, @shanksauce) 263 | * Added an example that uses generators (thanks @rudijs) 264 | * Fixed a link in the comments relating to heartbeats (thanks 265 | @tapickell) 266 | 267 | ## Changes in v0.4.2 268 | 269 | git log v0.4.1..v0.4.2 270 | 271 | * Better documentation and examples 272 | * Replace uses of ES6 keyword 'await' 273 | 274 | ## Changes in v0.4.1 275 | 276 | git log v0.4.0..v0.4.1 277 | 278 | * Tested in Node.JS 0.8 through 4.2 and 5.5 279 | * Emit an error with the 'close' event if server-initiated 280 | 281 | ## Changes in v0.4.0 282 | 283 | git log v0.3.2..v0.4.0 284 | 285 | * Tested on Node.JS 0.8 through 4.0 (and intervening io.js releases) 286 | * Change meaning of 'b' fields in tables to match RabbitMQ (and AMQP 287 | specification) 288 | * Can now pass an object in place of connection URL 289 | ([PR 159](https://github.com/squaremo/amqp.node/pull/159); thanks 290 | to @ben-page) 291 | * Operator-initiated connection close no longer results in 'error' 292 | event 293 | ([issue 110](https://github.com/squaremo/amqp.node/issues/110)) 294 | * Channel and Connection errors have now a `.code` field with the 295 | AMQP reply-code, which may help distinguish error cases 296 | ([PR 150](https://github.com/squaremo/amqp.node/pull/150); thanks 297 | to @hippich) 298 | * Connection.close will resolve to an error if the connection is 299 | already closed 300 | ([issue 181](https://github.com/squaremo/amqp.node/issues/181)) 301 | * Connection establishment will resolve with an error if the 302 | TCP-level connection or the handshake times out 303 | ([PR 169](https://github.com/squaremo/amqp.node/pull/169); thanks 304 | to @zweifisch and @RoCat, who both submitted fixes) 305 | * Add the `maxPriority` option as an alias for the `'x-max-priority'` 306 | queue argument 307 | ([PR 180](https://github.com/squaremo/amqp.node/pull/180); thanks 308 | to @ebardes) 309 | 310 | ## Changes in v0.3.2 (since v0.3.1) 311 | 312 | git log v0.3.1..v0.3.2 313 | 314 | * Make the engine specification more flexible to admit io.js releases 315 | 316 | ## Changes in v0.3.1 (since v0.3.0) 317 | 318 | git log v0.3.0..v0.3.1 319 | 320 | ### Fixes 321 | 322 | * Fail in the right way when a channel cannot be allocated [issue 323 | 129](https://github.com/squaremo/amqp.node/issues/129) 324 | * Make `waitForConfirms` work properly in callback API [PR 325 | 116](https://github.com/squaremo/amqp.node/pull/116) 326 | 327 | ### Enhancements 328 | 329 | * Two new options while connecting: 330 | [timeout](https://github.com/squaremo/amqp.node/pull/118) and [keep 331 | alive](https://github.com/squaremo/amqp.node/pull/125) (thanks to 332 | @rexxars and @jcrugzz respectively) 333 | 334 | ## Changes in v0.3.0 (since v0.2.1) 335 | 336 | git log v0.2.1..v0.3.0 337 | 338 | ### Enhancements 339 | 340 | * Allow additional client properties to be set for a connection 341 | [Issue 98](https://github.com/squaremo/amqp.node/issues/98) and 342 | [PR 80](https://github.com/squaremo/amqp.node/pull/80) 343 | * New method in channel API to wait for all unconfirmed messages 344 | [Issue 89](https://github.com/squaremo/amqp.node/issues/89) 345 | * Now supports RabbitMQ's `EXTERNAL` authentication plugin 346 | [Issue 105](https://github.com/squaremo/amqp.node/issues/105) 347 | 348 | ## Changes in v0.2.1 (since v0.2.0) 349 | 350 | ### Fixes 351 | 352 | * Do tuning negotation properly [PR 353 | 84](https://github.com/squaremo/amqp.node/pull/84) 354 | 355 | ## Changes in v0.2.0 (since v0.1.3) 356 | 357 | git log v0.1.3..v0.2.0 358 | 359 | ### Fixes 360 | 361 | * Correctly deal with missing fields (issue 48) 362 | 363 | ### Enhancements 364 | 365 | * Added a callback-oriented API, parallel to the existing, 366 | promise-oriented API. 367 | * The response to assertExchange now contains the exchange name, 368 | analagous to assertQueue (issue 49) 369 | * The channel method `prefetch` now has a global flag, to be 370 | [compatible with newer RabbitMQ][rabbitmq-prefetch-global]. 371 | 372 | ## Changes in v0.1.3 (since v0.1.2) 373 | 374 | git log v0.1.2..v0.1.3 375 | 376 | ### Enhancements 377 | 378 | * Add support in the API for using Basic.Reject rather than 379 | Basic.Nack, the latter of which is a RabbitMQ extension and not in 380 | older versions of RabbitMQ. 381 | 382 | ## Changes in v0.1.2 (since v0.1.1) 383 | 384 | git log v0.1.1..v0.1.2 385 | 386 | ### Fixes 387 | 388 | * Restore support for publishing zero-length messages 389 | 390 | ### Enhancements 391 | 392 | * Recognise [authentication failures][rabbitmq-auth-failure] 393 | * An option to set TCP_NODELAY on connection sockets 394 | 395 | ## Changes in v0.1.1 (since v0.1.0) 396 | 397 | git log v0.1.0..v0.1.1 398 | 399 | ### Fixes 400 | 401 | * Safer frame construction, no longer relies on allocating a large, 402 | fixed-size buffer and hoping it's big enough 403 | * The ports of RabbitMQ tutorials now avoid a race between publishing 404 | and closing the connection 405 | 406 | ### Enhancements 407 | 408 | * Support for RabbitMQ's consumer priority extension 409 | * Support for RabbitMQ's connnection.blocked extension 410 | * Better write speed from batching frames for small messages 411 | * Other minor efficiency gains in method encoding and decoding 412 | * Channel and connection state errors (e.g., trying to write when 413 | closed) include a stack trace from when they moved to that state 414 | * The `arguments` table, passed as an option to some methods, can 415 | include fields in its prototype chain 416 | * Provide the more accurately named `persistent` as a near equivalent 417 | of `deliveryMode` 418 | 419 | ## Changes in v0.1.0 (since v0.0.2) 420 | 421 | git log v0.0.2..v0.1.0 422 | 423 | ### Breaking changes 424 | 425 | * Consumer callbacks are invoked with `null` if the consumer is 426 | cancelled (see 427 | [RabbitMQ's consumer cancel notification][rabbitmq-consumer-cancel]) 428 | * In confirm channels, instead of `#publish` and `#sendToQueue` 429 | returning promises, they return a boolean as for normal channels, 430 | and take a Node.JS-style `function (err, ok)` callback for the 431 | server ack or nack 432 | 433 | ### Fixes 434 | 435 | * Overlapping channel and connection close frames are dealt with 436 | gracefully 437 | * Exceptions thrown in consumer callbacks are raised as `'error'` 438 | events 439 | * Zero-size messages are handled 440 | * Avoid monkey-patching `Buffer`, and eschew 441 | `require('util')._extend` 442 | 443 | ### Enhancements 444 | 445 | * Channels now behave like `Writable` streams with regard to `#publish` 446 | and `#sendToQueue`, returning a boolean from those methods and 447 | emitting `'drain'` 448 | * Connections now multiplex frames from channels fairly 449 | * Low-level channel machinery is now fully callback-based 450 | 451 | 452 | [rabbitmq-consumer-cancel]: http://www.rabbitmq.com/consumer-cancel.html 453 | [rabbitmq-auth-failure]: http://www.rabbitmq.com/auth-notification.html 454 | [rabbitmq-prefetch-global]: http://www.rabbitmq.com/consumer-prefetch.html 455 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | amqplib copyright (c) 2013, 2014 2 | Michael Bridgen 3 | 4 | This package, "amqplib", is licensed under the MIT License. A copy may 5 | be found in the file LICENSE-MIT in this directory, or downloaded from 6 | http://opensource.org/licenses/MIT. 7 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013, 2014 Michael Bridgen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RABBITMQ_SRC_VERSION=v3.12.13 2 | JSON=amqp-rabbitmq-0.9.1.json 3 | AMQP_JSON=https://raw.githubusercontent.com/rabbitmq/rabbitmq-server/$(RABBITMQ_SRC_VERSION)/deps/rabbitmq_codegen/$(JSON) 4 | 5 | NODEJS_VERSIONS='10.21' '11.15' '12.18' '13.14' '14.5' '15.8' '16.3.0' '18.1.0' '20.10.0' '22.14.0' 6 | 7 | MOCHA=./node_modules/.bin/mocha 8 | _MOCHA=./node_modules/.bin/_mocha 9 | UGLIFY=./node_modules/.bin/uglifyjs 10 | NYC=./node_modules/.bin/nyc 11 | 12 | .PHONY: test test-all-nodejs coverage lib/defs.js 13 | 14 | error: 15 | @echo "Please choose one of the following targets: test, test-all-nodejs, coverage, lib/defs.js" 16 | @exit 1 17 | 18 | test: 19 | $(MOCHA) --check-leaks -u tdd --exit test/ 20 | 21 | test-all-nodejs: 22 | for v in $(NODEJS_VERSIONS); \ 23 | do echo "-- Node version $$v --"; \ 24 | nave use $$v $(MOCHA) -u tdd --exit -R progress test; \ 25 | done 26 | 27 | coverage: $(NYC) 28 | $(NYC) --clean --reporter=lcov --reporter=text $(_MOCHA) -u tdd --exit -R progress test/ 29 | @echo "HTML report at file://$$(pwd)/coverage/lcov-report/index.html" 30 | 31 | lib/defs.js: clean bin/generate-defs test 32 | 33 | clean: 34 | rm -f lib/defs.js bin/amqp-rabbitmq-0.9.1.json 35 | 36 | bin/generate-defs: $(UGLIFY) bin/generate-defs.js bin/amqp-rabbitmq-0.9.1.json 37 | (cd bin; node ./generate-defs.js > ../lib/defs.js) 38 | $(UGLIFY) ./lib/defs.js -o ./lib/defs.js \ 39 | -c 'sequences=false' --comments \ 40 | -b 'indent-level=2' 2>&1 | (grep -v 'WARN' || true) 41 | 42 | bin/amqp-rabbitmq-0.9.1.json: 43 | curl -L $(AMQP_JSON) > $@ 44 | 45 | $(ISTANBUL): 46 | npm install 47 | 48 | $(UGLIFY): 49 | npm install 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AMQP 0-9-1 library and client for Node.JS 2 | 3 | [![NPM version](https://img.shields.io/npm/v/amqplib.svg?style=flat-square)](https://www.npmjs.com/package/amqplib) 4 | [![NPM downloads](https://img.shields.io/npm/dm/amqplib.svg?style=flat-square)](https://www.npmjs.com/package/amqplib) 5 | [![Node.js CI](https://github.com/amqp-node/amqplib/workflows/Node.js%20CI/badge.svg)](https://github.com/amqp-node/amqplib/actions?query=workflow%3A%22Node.js+CI%22) 6 | [![amqplib](https://snyk.io/advisor/npm-package/amqplib/badge.svg)](https://snyk.io/advisor/npm-package/amqplib) 7 | 8 | A library for making AMQP 0-9-1 clients for Node.JS, and an AMQP 0-9-1 client for Node.JS v10+. This library does not implement [AMQP1.0](https://github.com/amqp-node/amqplib/issues/63) or [AMQP0-10](https://github.com/amqp-node/amqplib/issues/94). 9 | 10 | npm install amqplib 11 | 12 | ## ❤️ Help Support Jack 13 | One of my close friends, Jack, was recently diagnosed with terminal brain cancer (grade 4 astrocytoma). He’s a young father facing an unimaginably tough road with remarkable courage. Thanks to chemotherapy, radiotherapy, and your support, two of his tumours have stopped showing activity. Donations help Jack continue accessing promising complementary therapies, attend hospital appointments, and spend meaningful time with his children. 14 | 15 | If you’ve benefited from amqplib, please consider supporting Jack’s journey through his [J Crushing Cancer](https://www.gofundme.com/f/j-crushing-cancer) gofundme page. Thank you - @cressie176 16 | 17 | ## RabbitMQ Compatibility 18 | 19 | Only `0.10.7` and later versions of this library are compatible with RabbitMQ 4.1.0 (and later releases). 20 | 21 | ## Links 22 | * [Change log][changelog] 23 | * [GitHub pages][gh-pages] 24 | * [API reference][gh-pages-apiref] 25 | * [Troubleshooting][gh-pages-trouble] 26 | * [Examples from RabbitMQ tutorials][tutes] 27 | 28 | ## Project status 29 | 30 | - Expected to work 31 | - Complete high-level and low-level APIs (i.e., all bits of the protocol) 32 | - Stable APIs 33 | - A fair few tests 34 | - Measured test coverage 35 | - Ports of the [RabbitMQ tutorials][rabbitmq-tutes] as [examples][tutes] 36 | - Used in production 37 | 38 | Still working on: 39 | 40 | - Getting to 100% (or very close to 100%) test coverage 41 | 42 | ## Callback API example 43 | 44 | ```javascript 45 | const amqplib = require('amqplib/callback_api'); 46 | const queue = 'tasks'; 47 | 48 | amqplib.connect('amqp://localhost', (err, conn) => { 49 | if (err) throw err; 50 | 51 | // Listener 52 | conn.createChannel((err, ch2) => { 53 | if (err) throw err; 54 | 55 | ch2.assertQueue(queue); 56 | 57 | ch2.consume(queue, (msg) => { 58 | if (msg !== null) { 59 | console.log(msg.content.toString()); 60 | ch2.ack(msg); 61 | } else { 62 | console.log('Consumer cancelled by server'); 63 | } 64 | }); 65 | }); 66 | 67 | // Sender 68 | conn.createChannel((err, ch1) => { 69 | if (err) throw err; 70 | 71 | ch1.assertQueue(queue); 72 | 73 | setInterval(() => { 74 | ch1.sendToQueue(queue, Buffer.from('something to do')); 75 | }, 1000); 76 | }); 77 | }); 78 | ``` 79 | 80 | ## Promise/Async API example 81 | 82 | ```javascript 83 | const amqplib = require('amqplib'); 84 | 85 | (async () => { 86 | const queue = 'tasks'; 87 | const conn = await amqplib.connect('amqp://localhost'); 88 | 89 | const ch1 = await conn.createChannel(); 90 | await ch1.assertQueue(queue); 91 | 92 | // Listener 93 | ch1.consume(queue, (msg) => { 94 | if (msg !== null) { 95 | console.log('Received:', msg.content.toString()); 96 | ch1.ack(msg); 97 | } else { 98 | console.log('Consumer cancelled by server'); 99 | } 100 | }); 101 | 102 | // Sender 103 | const ch2 = await conn.createChannel(); 104 | 105 | setInterval(() => { 106 | ch2.sendToQueue(queue, Buffer.from('something to do')); 107 | }, 1000); 108 | })(); 109 | 110 | ``` 111 | 112 | ## Running tests 113 | 114 | npm test 115 | 116 | To run the tests RabbitMQ is required. Either install it with your package 117 | manager, or use [docker][] to run a RabbitMQ instance. 118 | 119 | docker run -d --name amqp.test -p 5672:5672 rabbitmq 120 | 121 | If prefer not to run RabbitMQ locally it is also possible to use a 122 | instance of RabbitMQ hosted elsewhere. Use the `URL` environment 123 | variable to configure a different amqp host to connect to. You may 124 | also need to do this if docker is not on localhost; e.g., if it's 125 | running in docker-machine. 126 | 127 | One public host is dev.rabbitmq.com: 128 | 129 | URL=amqp://dev.rabbitmq.com npm test 130 | 131 | **NB** You may experience test failures due to timeouts if using the 132 | dev.rabbitmq.com instance. 133 | 134 | You can run it under different versions of Node.JS using [nave][]: 135 | 136 | nave use 10 npm test 137 | 138 | or run the tests on all supported versions of Node.JS in one go: 139 | 140 | make test-all-nodejs 141 | 142 | (which also needs `nave` installed, of course). 143 | 144 | Lastly, setting the environment variable `LOG_ERRORS` will cause the 145 | tests to output error messages encountered, to the console; this is 146 | really only useful for checking the kind and formatting of the errors. 147 | 148 | LOG_ERRORS=true npm test 149 | 150 | ## Test coverage 151 | 152 | make coverage 153 | open file://`pwd`/coverage/lcov-report/index.html 154 | 155 | [gh-pages]: https://amqp-node.github.io/amqplib/ 156 | [gh-pages-apiref]: https://amqp-node.github.io/amqplib/channel_api.html 157 | [gh-pages-trouble]: https://amqp-node.github.io/amqplib/#troubleshooting 158 | [nave]: https://github.com/isaacs/nave 159 | [tutes]: https://github.com/amqp-node/amqplib/tree/main/examples/tutorials 160 | [rabbitmq-tutes]: http://www.rabbitmq.com/getstarted.html 161 | [changelog]: https://github.com/amqp-node/amqplib/blob/main/CHANGELOG.md 162 | [docker]: https://www.docker.com/ 163 | -------------------------------------------------------------------------------- /callback_api.js: -------------------------------------------------------------------------------- 1 | var raw_connect = require('./lib/connect').connect; 2 | var CallbackModel = require('./lib/callback_model').CallbackModel; 3 | 4 | // Supports three shapes: 5 | // connect(url, options, callback) 6 | // connect(url, callback) 7 | // connect(callback) 8 | function connect(url, options, cb) { 9 | if (typeof url === 'function') 10 | cb = url, url = false, options = false; 11 | else if (typeof options === 'function') 12 | cb = options, options = false; 13 | 14 | raw_connect(url, options, function(err, c) { 15 | if (err === null) cb(null, new CallbackModel(c)); 16 | else cb(err); 17 | }); 18 | }; 19 | 20 | module.exports.connect = connect; 21 | module.exports.credentials = require('./lib/credentials'); 22 | module.exports.IllegalOperationError = require('./lib/error').IllegalOperationError; 23 | -------------------------------------------------------------------------------- /channel_api.js: -------------------------------------------------------------------------------- 1 | var raw_connect = require('./lib/connect').connect; 2 | var ChannelModel = require('./lib/channel_model').ChannelModel; 3 | var promisify = require('util').promisify; 4 | 5 | function connect(url, connOptions) { 6 | return promisify(function(cb) { 7 | return raw_connect(url, connOptions, cb); 8 | })() 9 | .then(function(conn) { 10 | return new ChannelModel(conn); 11 | }); 12 | }; 13 | 14 | module.exports.connect = connect; 15 | module.exports.credentials = require('./lib/credentials'); 16 | module.exports.IllegalOperationError = require('./lib/error').IllegalOperationError; 17 | -------------------------------------------------------------------------------- /examples/direct_reply_to_client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('../'); 4 | 5 | const queue = 'rpc_queue'; 6 | 7 | (async () => { 8 | const connection = await amqp.connect(); 9 | const channel = await connection.createChannel(); 10 | 11 | await channel.consume('amq.rabbitmq.reply-to', async (message) => { 12 | console.log(message.content.toString()); 13 | await channel.close(); 14 | await connection.close(); 15 | }, { noAck: true }); 16 | 17 | await channel.assertQueue(queue, { durable: false }); 18 | 19 | channel.sendToQueue(queue, Buffer.from(' [X] ping'), { 20 | replyTo: 'amq.rabbitmq.reply-to', 21 | }); 22 | })(); 23 | -------------------------------------------------------------------------------- /examples/direct_reply_to_server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('../'); 4 | const { v4: uuid } = require('uuid'); 5 | 6 | const queue = 'rpc_queue'; 7 | 8 | (async () => { 9 | const connection = await amqp.connect(); 10 | const channel = await connection.createChannel(); 11 | 12 | process.once('SIGINT', async () => { 13 | await channel.close(); 14 | await connection.close(); 15 | }); 16 | 17 | await channel.assertQueue(queue, { durable: false }); 18 | await channel.consume(queue, (message) => { 19 | console.log(message.content.toString()); 20 | channel.sendToQueue(message.properties.replyTo, Buffer.from(' [.] pong')); 21 | }, { noAck: true }); 22 | 23 | console.log(' [x] To exit press CTRL+C.'); 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /examples/headers.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('../'); 4 | 5 | (async () => { 6 | 7 | const connection = await amqp.connect(); 8 | const channel = await connection.createChannel(); 9 | 10 | process.once('SIGINT', async () => { 11 | await channel.close(); 12 | await connection.close(); 13 | }); 14 | 15 | const { exchange } = await channel.assertExchange('matching exchange', 'headers'); 16 | const { queue } = await channel.assertQueue(); 17 | 18 | // When using a headers exchange, the headers to be matched go in 19 | // the binding arguments. The routing key is ignore, so best left 20 | // empty. 21 | 22 | // 'x-match' is 'all' or 'any', meaning "all fields must match" or 23 | // "at least one field must match", respectively. The values to be 24 | // matched go in subsequent fields. 25 | await channel.bindQueue(queue, exchange, '', { 26 | 'x-match': 'any', 27 | 'foo': 'bar', 28 | 'baz': 'boo' 29 | }); 30 | 31 | await channel.consume(queue, (message) => { 32 | console.log(message.content.toString()); 33 | }, { noAck: true }); 34 | 35 | channel.publish(exchange, '', Buffer.from('hello'), { headers: { baz: 'boo' }}); 36 | channel.publish(exchange, '', Buffer.from('hello'), { headers: { foo: 'bar' }}); 37 | channel.publish(exchange, '', Buffer.from('lost'), { headers: { meh: 'nah' }}); 38 | 39 | console.log(' [x] To exit press CTRL+C.'); 40 | })(); 41 | -------------------------------------------------------------------------------- /examples/receive_generator.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | const co = require('co'); 5 | const amqp = require('amqplib'); 6 | const readline = require('readline'); 7 | 8 | co(function* () { 9 | const myConsumer = (msg) => { 10 | if (msg !== null) { 11 | console.log('consuming message %s in generator', JSON.stringify(msg.content.toString())); 12 | } 13 | }; 14 | const conn = yield amqp.connect('amqp://localhost'); 15 | try { 16 | // create a message to consume 17 | const q = 'hello'; 18 | const msg = 'Hello World!'; 19 | const channel = yield conn.createChannel(); 20 | yield channel.assertQueue(q); 21 | channel.sendToQueue(q, Buffer.from(msg)); 22 | console.log(" [x] Sent '%s'", msg); 23 | // consume the message 24 | yield channel.consume(q, myConsumer, { noAck: true }); 25 | } 26 | catch (e) { 27 | throw e; 28 | } 29 | }).catch(err => { 30 | console.warn('Error:', err); 31 | }); 32 | 33 | const rl = readline.createInterface({ 34 | input: process.stdin, 35 | output: process.stdout 36 | }); 37 | 38 | // pend until message is consumed 39 | rl.question('newline to exit', () => process.exit()); -------------------------------------------------------------------------------- /examples/send_generators.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | // NB this requires the module 'co': 6 | // npm install co 7 | const co = require('co'); 8 | const amqp = require('amqplib'); 9 | 10 | co(function* () { 11 | // connection errors are handled in the co .catch handler 12 | const conn = yield amqp.connect('amqp://localhost'); 13 | 14 | // try catch will throw any errors from the yielding the following promises to the co .catch handler 15 | try { 16 | const q = 'hello'; 17 | const msg = 'Hello World!'; 18 | 19 | // use a confirm channel so we can check the message is sent OK. 20 | const channel = yield conn.createConfirmChannel(); 21 | 22 | yield channel.assertQueue(q); 23 | 24 | channel.sendToQueue(q, Buffer.from(msg)); 25 | 26 | // if message has been nacked, this will result in an error (rejected promise); 27 | yield channel.waitForConfirms(); 28 | 29 | console.log(" [x] Sent '%s'", msg); 30 | 31 | channel.close(); 32 | } 33 | catch (e) { 34 | throw e; 35 | } 36 | finally { 37 | conn.close(); 38 | } 39 | 40 | }).catch(err => { 41 | console.warn('Error:', err); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/ssl.js: -------------------------------------------------------------------------------- 1 | // Example of using a TLS/SSL connection. Note that the server must be 2 | // configured to accept SSL connections; see, for example, 3 | // http://www.rabbitmq.com/ssl.html. 4 | // 5 | // When trying this out, I followed the RabbitMQ SSL guide above, 6 | // almost verbatim. I set the CN of the server certificate to 7 | // 'localhost' rather than $(hostname) (since on my MBP hostname ends 8 | // up being ".local", which is just weird). My client 9 | // certificates etc., are in `../etc/client/`. My testca certificate 10 | // is in `../etc/testca` and server certs etc., in `../etc/server`, 11 | // and I've made a `rabbitmq.config` file, with which I start 12 | // RabbitMQ: 13 | // 14 | // RABBITMQ_CONFIG_FILE=`pwd`/../etc/server/rabbitmq \ 15 | // /usr/local/sbin/rabbitmq-server & 16 | // 17 | // A way to check RabbitMQ's running with SSL OK is to use 18 | // 19 | // openssl s_client -connect localhost:5671 20 | 21 | const amqp = require('../'); 22 | const fs = require('fs'); 23 | 24 | // Assemble the SSL options; for verification we need at least 25 | // * a certificate to present to the server ('cert', in PEM format) 26 | // * the private key for the certificate ('key', in PEM format) 27 | // * (possibly) a passphrase for the private key 28 | // 29 | // The first two may be replaced with a PKCS12 file ('pfx', in pkcs12 30 | // format) 31 | 32 | // We will also want to list the CA certificates that we will trust, 33 | // since we're using a self-signed certificate. It is NOT recommended 34 | // to use `rejectUnauthorized: false`. 35 | 36 | // Options for full client and server verification: 37 | const opts = { 38 | cert: fs.readFileSync('../etc/client/cert.pem'), 39 | key: fs.readFileSync('../etc/client/key.pem'), 40 | // cert and key or 41 | // pfx: fs.readFileSync('../etc/client/keycert.p12'), 42 | passphrase: 'MySecretPassword', 43 | ca: [fs.readFileSync('../etc/testca/cacert.pem')] 44 | }; 45 | 46 | // Options for just confidentiality. This requires RabbitMQ's SSL 47 | // configuration to include the items 48 | // 49 | // {verify, verify_none}, 50 | // {fail_if_no_peer_cert,false} 51 | // 52 | // const opts = { ca: [fs.readFileSync('../etc/testca/cacert.pem')] }; 53 | 54 | // Option to use the SSL client certificate for authentication 55 | // opts.credentials = amqp.credentials.external(); 56 | 57 | (async () => { 58 | const connection = await amqp.connect('amqp://localhost', opts); 59 | const channel = await connection.createChannel(); 60 | 61 | process.on('SIGINT', async () => { 62 | await channel.close(); 63 | await connection.close(); 64 | }); 65 | 66 | channel.sendToQueue('foo', Buffer.from('Hello World!')); 67 | 68 | console.log(' [x] To exit press CTRL+C.'); 69 | })(); 70 | 71 | -------------------------------------------------------------------------------- /examples/stream_queues/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /examples/stream_queues/README.md: -------------------------------------------------------------------------------- 1 | RabbitMQ Stream Examples 2 | --- 3 | The [stream queues](https://www.rabbitmq.com/streams.html) are available starting from RabbitMQ 3.9 4 | 5 | These examples show how to use stream queues with the lib. 6 | 7 | Send a message to a stream queue 8 | ``` 9 | node send_stream.js 10 | ``` 11 | 12 | Receive all the messages from stream queue: 13 | ``` 14 | node receive_stream.js 15 | ``` 16 | 17 | Consumers can be configured to receive messages using an offset via the `x-stream-offset` argument. e.g. 18 | 19 | ```js 20 | channel.consume(queue, onMessage, { 21 | noAck: false, 22 | arguments: { 23 | 'x-stream-offset': 'first' 24 | } 25 | }); 26 | ``` 27 | 28 | RabbitMQ supports six different types of offset, however specifying them can be 29 | 30 | | Offset Type | Example Value | Notes | 31 | |-----------|----------------------------------------------------------|-------| 32 | | First | `{'x-stream-offset':'first'}` | Start from the first message in the log | 33 | | Last | `{'x-stream-offset':'last'}` | Start from the last "chunk" of messages (could be multiple messages) | 34 | | Next | `{'x-stream-offset':'next'}` | Start from the next message (the default) | 35 | | Offset | `{'x-stream-offset':5}` | a numerical value specifying an exact offset to attach to the log at | 36 | | Timestamp | `{'x-stream-offset':{'!':'timestamp',value:1686519750}}` | a timestamp value specifying the point in time to attach to the log at. The timestamp must be the number of seconds since 00:00:00 UTC, 1970-01-01. Consumers can receive messages published a bit before the specified timestamp. | 37 | | Interval | `{'x-stream-offset':'1h'}` | the time interval relative to current time to attach the log at. Valid units are Y, M, D, h, m and s | 38 | 39 | 40 | See https://www.rabbitmq.com/streams.html#consuming for more details 41 | -------------------------------------------------------------------------------- /examples/stream_queues/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream_queues", 3 | "version": "1.0.0", 4 | "description": "An example demonstrating use of stream queues", 5 | "main": "n", 6 | "dependencies": { 7 | "amqplib": "^0.10.3" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /examples/stream_queues/receive_stream.js: -------------------------------------------------------------------------------- 1 | const amqp = require('amqplib'); 2 | 3 | (async () => { 4 | try { 5 | const connection = await amqp.connect('amqp://localhost'); 6 | process.once('SIGINT', connection.close); 7 | 8 | const channel = await connection.createChannel(); 9 | const queue = 'my_first_stream'; 10 | 11 | // Define the queue stream 12 | // Mandatory: exclusive: false, durable: true autoDelete: false 13 | await channel.assertQueue(queue, { 14 | exclusive: false, 15 | durable: true, 16 | autoDelete: false, 17 | arguments: { 18 | 'x-queue-type': 'stream', // Mandatory to define stream queue 19 | 'x-max-length-bytes': 2_000_000_000 // Set the queue retention to 2GB else the stream doesn't have any limit 20 | } 21 | }); 22 | 23 | channel.qos(100); // This is mandatory 24 | 25 | channel.consume(queue, (msg) => { 26 | console.log(" [x] Received '%s'", msg.content.toString()); 27 | channel.ack(msg); // Mandatory 28 | }, { 29 | noAck: false, 30 | arguments: { 31 | /* 32 | Here you can specify the offset: : first, last, next, offset, timestamp and interval, i.e. 33 | 34 | 'x-stream-offset': 'first' 35 | 'x-stream-offset': 'last' 36 | 'x-stream-offset': 'next' 37 | 'x-stream-offset': 5 38 | 'x-stream-offset': { '!': 'timestamp', value: 1686519750 } 39 | 'x-stream-offset': '1h' 40 | 41 | The timestamp must be the desired number of seconds since 00:00:00 UTC, 1970-01-01 42 | The interval units can be Y, M, D, h, m, s 43 | 44 | */ 45 | 'x-stream-offset': 'first' 46 | } 47 | }); 48 | 49 | console.log(' [*] Waiting for messages. To exit press CTRL+C'); 50 | } 51 | // Catch and display any errors in the console 52 | catch(e) { 53 | console.log(e) 54 | } 55 | })(); 56 | -------------------------------------------------------------------------------- /examples/stream_queues/send_stream.js: -------------------------------------------------------------------------------- 1 | const amqp = require('amqplib'); 2 | 3 | 4 | (async () => { 5 | try { 6 | const connection = await amqp.connect('amqp://localhost'); 7 | process.once('SIGINT', connection.close); 8 | 9 | const channel = await connection.createChannel(); 10 | const queue = 'my_first_stream'; 11 | const msg = `Hello World! ${Date.now()}`; 12 | 13 | // Define the queue stream 14 | // Mandatory: exclusive: false, durable: true autoDelete: false 15 | await channel.assertQueue(queue, { 16 | exclusive: false, 17 | durable: true, 18 | autoDelete: false, 19 | arguments: { 20 | 'x-queue-type': 'stream', // Mandatory to define stream queue 21 | 'x-max-length-bytes': 2_000_000_000 // Set the queue retention to 2GB else the stream doesn't have any limit 22 | } 23 | }); 24 | 25 | // Send the message to the stream queue 26 | await channel.sendToQueue(queue, Buffer.from(msg)); 27 | console.log(" [x] Sent '%s'", msg); 28 | await channel.close(); 29 | 30 | // Close connection 31 | connection.close(); 32 | } 33 | // Catch and display any errors in the console 34 | catch(e) { 35 | console.log(e) 36 | } 37 | })(); 38 | 39 | -------------------------------------------------------------------------------- /examples/tutorials/README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ tutorials 2 | 3 | This directory contains the [RabbitMQ tutorials][rabbitmq-tutes], 4 | ported to amqplib. The sub-directory `callback_api` has translations 5 | of the tutorial programs to the callback-oriented API. 6 | 7 | ## Preparation 8 | 9 | To run the tutorial code, you need amqplib installed. Assuming you are 10 | in a clone of the amqplib repository, from the tutorials directory: 11 | 12 | npm install 13 | 14 | or to use the latest released version, 15 | 16 | npm install amqplib 17 | 18 | Then just run each file as a script, e.g., in bash 19 | 20 | ./send.js 21 | 22 | or 23 | 24 | node send.js 25 | 26 | or 27 | 28 | nave use 0.8 node send.js 29 | 30 | ## [Tutorial one: Hello World!][tute-one] 31 | 32 | A "Hello World" example, with one script sending a message to a queue, 33 | and another receiving messages from the same queue. 34 | 35 | * [send.js](send.js) 36 | * [receive.js](receive.js) 37 | 38 | ## [Tutorial two: Work queues][tute-two] 39 | 40 | Using RabbitMQ as a work queue; `new_task` creates a task, and 41 | `worker` processes tasks. Multiple `worker` process will share the 42 | tasks among them. Long-running tasks are simulated by supplying a 43 | string with dots, e.g., '...' to `new_task`. Each dot makes the worker 44 | "work" for a second. 45 | 46 | * [new_task.js](new_task.js) 47 | * [worker.js](worker.js) 48 | 49 | ## [Tutorial three: Publish/Subscribe][tute-three] 50 | 51 | Using RabbitMQ as a broadcast mechanism. `emit_log` sends a "log" 52 | message to a fanout exchange, and all `receive_logs` processes receive 53 | log messages. 54 | 55 | * [emit_log.js](emit_log.js) 56 | * [receive_logs.js](receive_logs.js) 57 | 58 | ## [Tutorial four: Routing][tute-four] 59 | 60 | Using RabbitMQ as a routing ('somecast') mechanism. `emit_log_direct` 61 | sends a log message with a severity, and all `receive_logs_direct` 62 | processes receive log messages for the severities on which they are 63 | listening. 64 | 65 | * [emit_log_direct.js](emit_log_direct.js) 66 | * [receive_logs_direct.js](receive_logs_direct.js) 67 | 68 | ## [Tutorial five: Topics][tute-five] 69 | 70 | Extends the previous tutorial to routing with wildcarded patterns. 71 | 72 | * [emit_log_topic.js](emit_log_topic.js) 73 | * [receive_logs_topic.js](receive_logs_topic.js) 74 | 75 | ## [Tutorial six: RPC][tute-six] 76 | 77 | Using RabbitMQ as an RPC intermediary, queueing requests for servers 78 | and routing replies back to clients. 79 | 80 | * [rpc_server.js](rpc_server.js) 81 | * [rpc_client.js](rpc_client.js) 82 | 83 | I depart slightly from the original tutorial code, which I think has 84 | some needless object-orientation (in the Python code; you don't get a 85 | choice about needless object-orientation in Java). 86 | 87 | [rabbitmq-tutes]: http://github.com/rabbitmq/rabbitmq-tutorials 88 | [tute-one]: http://www.rabbitmq.com/tutorials/tutorial-one-javascript.html 89 | [tute-two]: http://www.rabbitmq.com/tutorials/tutorial-two-javascript.html 90 | [tute-three]: http://www.rabbitmq.com/tutorials/tutorial-three-javascript.html 91 | [tute-four]: http://www.rabbitmq.com/tutorials/tutorial-four-javascript.html 92 | [tute-five]: http://www.rabbitmq.com/tutorials/tutorial-five-javascript.html 93 | [tute-six]: http://www.rabbitmq.com/tutorials/tutorial-six-javascript.html 94 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/emit_log.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const exchange = 'logs'; 6 | const text = process.argv.slice(2).join(' ') || 'info: Hello World!'; 7 | 8 | amqp.connect((err, connection) => { 9 | if (err) return bail(err); 10 | connection.createChannel((err, channel) => { 11 | if (err) return bail(err, connection); 12 | channel.assertExchange(exchange, 'fanout', { durable: false }, (err) => { 13 | if (err) return bail(err, connection); 14 | channel.publish(exchange, '', Buffer.from(text)); 15 | console.log(" [x] Sent '%s'", text); 16 | channel.close(() => { 17 | connection.close(); 18 | }); 19 | }); 20 | }); 21 | }); 22 | 23 | function bail(err, connection) { 24 | console.error(err); 25 | if (connection) connection.close(() => { 26 | process.exit(1); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/emit_log_direct.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const exchange = 'direct_logs'; 6 | const args = process.argv.slice(2); 7 | const routingKey = (args.length > 0) ? args[0] : 'info'; 8 | const text = args.slice(1).join(' ') || 'Hello World!'; 9 | 10 | amqp.connect((err, connection) => { 11 | if (err) return bail(err); 12 | connection.createChannel((err, channel) => { 13 | if (err) return bail(err, connection); 14 | channel.assertExchange(exchange, 'direct', { durable: false }, (err) => { 15 | if (err) return bail(err, connection); 16 | channel.publish(exchange, routingKey, Buffer.from(text)); 17 | console.log(" [x] Sent '%s'", text); 18 | channel.close(() => { 19 | connection.close(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | 25 | function bail(err, connection) { 26 | console.error(err); 27 | if (connection) connection.close(() => { 28 | process.exit(1); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/emit_log_topic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const exchange = 'topic_logs'; 6 | const args = process.argv.slice(2); 7 | const routingKey = (args.length > 0) ? args[0] : 'info'; 8 | const text = args.slice(1).join(' ') || 'Hello World!'; 9 | 10 | amqp.connect((err, connection) => { 11 | if (err) return bail(err); 12 | connection.createChannel((err, channel) => { 13 | if (err) return bail(err, connection); 14 | channel.assertExchange(exchange, 'topic', { durable: false }, (err) => { 15 | if (err) return bail(err, connection); 16 | channel.publish(exchange, routingKey, Buffer.from(text)); 17 | console.log(" [x] Sent '%s'", text); 18 | channel.close(() => { 19 | connection.close(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | 25 | function bail(err, connection) { 26 | console.error(err); 27 | if (connection) connection.close(() => { 28 | process.exit(1); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/new_task.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const queue = 'task_queue'; 6 | const text = process.argv.slice(2).join(' ') || "Hello World!"; 7 | 8 | amqp.connect((err, connection) => { 9 | if (err) return bail(err); 10 | connection.createChannel((err, channel) => { 11 | if (err) return bail(err, connection); 12 | channel.assertQueue(queue, { durable: true }, (err) => { 13 | if (err) return bails(err, connection); 14 | channel.sendToQueue(queue, Buffer.from(text), { persistent: true }); 15 | console.log(" [x] Sent '%s'", text); 16 | channel.close(() => { 17 | connection.close(); 18 | }); 19 | }); 20 | }); 21 | }); 22 | 23 | function bail(err, connection) { 24 | console.error(err); 25 | if (connection) connection.close(() => { 26 | process.exit(1); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/receive.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const queue = 'hello'; 6 | 7 | amqp.connect((err, connection) => { 8 | if (err) return bail(err); 9 | connection.createChannel((err, channel) => { 10 | if (err) return bail(err, connection); 11 | 12 | process.once('SIGINT', () => { 13 | channel.close(() => { 14 | connection.close(); 15 | }); 16 | }); 17 | 18 | channel.assertQueue(queue, { durable: false }, (err) => { 19 | if (err) return bail(err, connection); 20 | channel.consume(queue, (message) => { 21 | if (message) console.log(" [x] Received '%s'", message.content.toString()); 22 | else console.warn(' [x] Consumer cancelled'); 23 | }, { noAck: true }, (err) => { 24 | if (err) return bail(err, connection); 25 | console.log(" [*] Waiting for logs. To exit press CTRL+C."); 26 | }); 27 | }); 28 | }); 29 | }); 30 | 31 | function bail(err, connection) { 32 | console.error(err); 33 | if (connection) connection.close(() => { 34 | process.exit(1); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/receive_logs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const exchange = 'logs'; 6 | 7 | amqp.connect((err, connection) => { 8 | if (err) return bail(err); 9 | connection.createChannel((err, channel) => { 10 | if (err) return bail(err, connection); 11 | 12 | process.once('SIGINT', () => { 13 | channel.close(() => { 14 | connection.close(); 15 | }); 16 | }); 17 | 18 | channel.assertExchange(exchange, 'fanout', { durable: false }, (err, { queue }) => { 19 | if (err) return bail(err, connection); 20 | channel.assertQueue('', { exclusive: true }, (err, { queue }) => { 21 | if (err) return bail(err, connection); 22 | channel.bindQueue(queue, exchange, '', {}, (err) => { 23 | if (err) return bail(err, connection); 24 | channel.consume(queue, (message) => { 25 | if (message) console.log(" [x] '%s'", message.content.toString()); 26 | else console.warn(' [x] Consumer cancelled'); 27 | }, { noAck: true }, (err) => { 28 | if (err) return bail(err, connection); 29 | console.log(" [*] Waiting for logs. To exit press CTRL+C."); 30 | }); 31 | }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | function bail(err, connection) { 38 | console.error(err); 39 | if (connection) connection.close(() => { 40 | process.exit(1); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/receive_logs_direct.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | const { basename } = require('path'); 5 | 6 | const exchange = 'direct_logs'; 7 | const severities = process.argv.slice(2); 8 | if (severities.length < 1) { 9 | console.log('Usage %s [info] [warning] [error]', basename(process.argv[1])); 10 | process.exit(1); 11 | } 12 | 13 | amqp.connect((err, connection) => { 14 | if (err) return bail(err); 15 | connection.createChannel((err, channel) => { 16 | if (err) return bail(err, connection); 17 | 18 | process.once('SIGINT', () => { 19 | channel.close(() => { 20 | connection.close(); 21 | }); 22 | }); 23 | 24 | channel.assertExchange(exchange, 'direct', { durable: false }, (err) => { 25 | if (err) return bail(err, connection); 26 | channel.assertQueue('', { exclusive: true }, (err, { queue }) => { 27 | if (err) return bail(err, connection); 28 | channel.consume(queue, (message) => { 29 | if (message) console.log(" [x] %s:'%s'", message.fields.routingKey, message.content.toString()); 30 | else console.warn(' [x] Consumer cancelled'); 31 | }, {noAck: true}, function(err) { 32 | if (err) return bail(err, connection); 33 | console.log(' [*] Waiting for logs. To exit press CTRL+C.'); 34 | subscribeAll(channel, queue, severities, (err) => { 35 | if (err) return bail(err, connection); 36 | }); 37 | }); 38 | }); 39 | }); 40 | }); 41 | }); 42 | 43 | function subscribeAll(channel, queue, bindingKeys, cb) { 44 | if (bindingKeys.length === 0) return cb(); 45 | const bindingKey = bindingKeys.shift(); 46 | channel.bindQueue(queue, exchange, bindingKey, {}, (err) => { 47 | if (err) return cb(err); 48 | subscribeAll(channel, queue, bindingKeys, cb); 49 | }); 50 | } 51 | 52 | function bail(err, connection) { 53 | console.error(err); 54 | if (connection) connection.close(() => { 55 | process.exit(1); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/receive_logs_topic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | const { basename } = require('path'); 5 | 6 | const exchange = 'topic_logs'; 7 | const severities = process.argv.slice(2); 8 | if (severities.length < 1) { 9 | console.log('Usage %s [info] [warning] [error]', basename(process.argv[1])); 10 | process.exit(1); 11 | } 12 | 13 | amqp.connect((err, connection) => { 14 | if (err) return bail(err); 15 | connection.createChannel((err, channel) => { 16 | if (err) return bail(err, connection); 17 | 18 | process.once('SIGINT', () => { 19 | channel.close(() => { 20 | connection.close(); 21 | }); 22 | }); 23 | 24 | channel.assertExchange(exchange, 'topic', { durable: false }, (err) => { 25 | if (err) return bail(err, connection); 26 | channel.assertQueue('', { exclusive: true }, (err, { queue }) => { 27 | if (err) return bail(err, connection); 28 | channel.consume(queue, (message) => { 29 | if (message) console.log(" [x] %s:'%s'", message.fields.routingKey, message.content.toString()); 30 | else console.warn(' [x] Consumer cancelled'); 31 | }, {noAck: true}, function(err) { 32 | if (err) return bail(err, connection); 33 | console.log(' [*] Waiting for logs. To exit press CTRL+C.'); 34 | subscribeAll(channel, queue, severities, (err) => { 35 | if (err) return bail(err, connection); 36 | }); 37 | }); 38 | }); 39 | }); 40 | }); 41 | }); 42 | 43 | function subscribeAll(channel, queue, bindingKeys, cb) { 44 | if (bindingKeys.length === 0) return cb(); 45 | const bindingKey = bindingKeys.shift(); 46 | channel.bindQueue(queue, exchange, bindingKey, {}, (err) => { 47 | if (err) return cb(err); 48 | subscribeAll(channel, queue, bindingKeys, cb); 49 | }); 50 | } 51 | 52 | function bail(err, connection) { 53 | console.error(err); 54 | if (connection) connection.close(() => { 55 | process.exit(1); 56 | }); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/rpc_client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | const { basename } = require('path'); 5 | const { v4: uuid } = require('uuid'); 6 | 7 | const queue = 'rpc_queue'; 8 | 9 | const n = parseInt(process.argv[2], 10); 10 | if (isNaN(n)) { 11 | console.warn('Usage: %s number', basename(process.argv[1])); 12 | process.exit(1); 13 | } 14 | 15 | amqp.connect((err, connection) => { 16 | if (err) return bail(err); 17 | connection.createChannel((err, channel) => { 18 | if (err) return bail(err, connection); 19 | channel.assertQueue('', { exclusive: true }, (err, { queue: replyTo }) => { 20 | if (err) return bail(err, connection); 21 | 22 | const correlationId = uuid(); 23 | channel.consume(replyTo, (message) => { 24 | if (!message) console.warn(' [x] Consumer cancelled'); 25 | else if (message.properties.correlationId === correlationId) { 26 | console.log(' [.] Got %d', message.content.toString()); 27 | channel.close(() => { 28 | connection.close(); 29 | }) 30 | } 31 | }, { noAck: true }); 32 | 33 | channel.assertQueue(queue, { durable: false }, (err) => { 34 | if (err) return bail(err, connection); 35 | console.log(' [x] Requesting fib(%d)', n); 36 | channel.sendToQueue(queue, Buffer.from(n.toString()), { 37 | correlationId, 38 | replyTo 39 | }); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | function bail(err, connection) { 46 | console.error(err); 47 | if (connection) connection.close(() => { 48 | process.exit(1); 49 | }); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/rpc_server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const queue = 'rpc_queue'; 6 | 7 | amqp.connect((err, connection) => { 8 | if (err) return bail(err); 9 | connection.createChannel((err, channel) => { 10 | if (err) return bail(err, connection); 11 | 12 | process.once('SIGINT', () => { 13 | channel.close(() => { 14 | connection.close(); 15 | }); 16 | }); 17 | 18 | channel.assertQueue(queue, { durable: false }, (err) => { 19 | if (err) return bail(err, connection); 20 | channel.prefetch(1); 21 | channel.consume(queue, (message) => { 22 | const n = parseInt(message.content.toString(), 10); 23 | console.log(' [.] fib(%d)', n); 24 | const response = fib(n); 25 | channel.sendToQueue(message.properties.replyTo, Buffer.from(response.toString()), { 26 | correlationId: message.properties.correlationId 27 | }); 28 | channel.ack(message); 29 | }, { noAck: false }, function(err) { 30 | if (err) return bail(err, conn); 31 | console.log(' [x] Awaiting RPC requests. To exit press CTRL+C.'); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | function fib(n) { 38 | // Do it the ridiculous, but not most ridiculous, way. For better, 39 | // see http://nayuki.eigenstate.org/page/fast-fibonacci-algorithms 40 | let a = 0, b = 1; 41 | for (let i=0; i < n; i++) { 42 | let c = a + b; 43 | a = b; b = c; 44 | } 45 | return a; 46 | } 47 | 48 | 49 | function bail(err, connection) { 50 | console.error(err); 51 | if (connection) connection.close(() => { 52 | process.exit(1); 53 | }); 54 | } 55 | 56 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/send.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const queue = 'hello'; 6 | const text = 'Hello World!'; 7 | 8 | amqp.connect((err, connection) => { 9 | if (err) return bail(err); 10 | connection.createChannel((err, channel) => { 11 | if (err) return bail(err, connection); 12 | channel.assertQueue(queue, { durable: false }, (err) => { 13 | if (err) return bail(err, connection); 14 | channel.sendToQueue(queue, Buffer.from(text)); 15 | console.log(" [x] Sent '%s'", text); 16 | channel.close(() => { 17 | connection.close(); 18 | }); 19 | }); 20 | }); 21 | }); 22 | 23 | function bail(err, connection) { 24 | console.error(err); 25 | if (connection) connection.close(() => { 26 | process.exit(1); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /examples/tutorials/callback_api/worker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib/callback_api'); 4 | 5 | const queue = 'task_queue'; 6 | 7 | amqp.connect((err, connection) => { 8 | if (err) return bail(err); 9 | connection.createChannel((err, channel) => { 10 | if (err) return bail(err, connection); 11 | 12 | process.once('SIGINT', () => { 13 | channel.close(() => { 14 | connection.close(); 15 | }); 16 | }); 17 | 18 | channel.assertQueue(queue, { durable: true }, (err, { queue }) => { 19 | if (err) return bail(err, connection); 20 | channel.consume(queue, (message) => { 21 | const text = message.content.toString(); 22 | console.log(" [x] Received '%s'", text); 23 | const seconds = text.split('.').length - 1; 24 | setTimeout(() => { 25 | console.log(" [x] Done"); 26 | channel.ack(message); 27 | }, seconds * 1000); 28 | }, { noAck: false }); 29 | console.log(" [*] Waiting for messages. To exit press CTRL+C"); 30 | }); 31 | }); 32 | }); 33 | 34 | function bail(err, connection) { 35 | console.error(err); 36 | if (connection) connection.close(() => { 37 | process.exit(1); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /examples/tutorials/emit_log.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const exchange = 'logs'; 6 | const text = process.argv.slice(2).join(' ') || 'info: Hello World!'; 7 | 8 | (async () => { 9 | let connection; 10 | try { 11 | connection = await amqp.connect('amqp://localhost'); 12 | const channel = await connection.createChannel(); 13 | await channel.assertExchange(exchange, 'fanout', { durable: false }); 14 | channel.publish(exchange, '', Buffer.from(text)); 15 | console.log(" [x] Sent '%s'", text); 16 | await channel.close(); 17 | } 18 | catch (err) { 19 | console.warn(err); 20 | } 21 | finally { 22 | if (connection) await connection.close(); 23 | }; 24 | })(); 25 | -------------------------------------------------------------------------------- /examples/tutorials/emit_log_direct.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const exchange = 'direct_logs'; 6 | const args = process.argv.slice(2); 7 | const routingKey = (args.length > 0) ? args[0] : 'info'; 8 | const text = args.slice(1).join(' ') || 'Hello World!'; 9 | 10 | (async () => { 11 | let connection; 12 | try { 13 | connection = await amqp.connect('amqp://localhost'); 14 | const channel = await connection.createChannel(); 15 | await channel.assertExchange(exchange, 'direct', { durable: false }); 16 | channel.publish(exchange, routingKey, Buffer.from(text)); 17 | console.log(" [x] Sent %s:'%s'", routingKey, text); 18 | await channel.close(); 19 | } 20 | catch (err) { 21 | console.warn(err); 22 | } 23 | finally { 24 | if (connection) await connection.close(); 25 | }; 26 | })(); 27 | -------------------------------------------------------------------------------- /examples/tutorials/emit_log_topic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const exchange = 'topic_logs'; 6 | const args = process.argv.slice(2); 7 | const routingKeys = (args.length > 0) ? args[0] : 'info'; 8 | const text = args.slice(1).join(' ') || 'Hello World!'; 9 | 10 | (async () => { 11 | let connection; 12 | try { 13 | connection = await amqp.connect('amqp://localhost'); 14 | const channel = await connection.createChannel(); 15 | await channel.assertExchange(exchange, 'topic', { durable: false }); 16 | channel.publish(exchange, routingKeys, Buffer.from(text)); 17 | console.log(" [x] Sent %s:'%s'", routingKeys, text); 18 | await channel.close(); 19 | } 20 | catch (err) { 21 | console.warn(err); 22 | } 23 | finally { 24 | if (connection) await connection.close(); 25 | }; 26 | })(); 27 | -------------------------------------------------------------------------------- /examples/tutorials/new_task.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Post a new task to the work queue 3 | 4 | const amqp = require('amqplib'); 5 | 6 | const queue = 'task_queue'; 7 | const text = process.argv.slice(2).join(' ') || "Hello World!"; 8 | 9 | (async () => { 10 | let connection; 11 | try { 12 | connection = await amqp.connect('amqp://localhost'); 13 | const channel = await connection.createChannel(); 14 | await channel.assertQueue(queue, { durable: true }); 15 | channel.sendToQueue(queue, Buffer.from(text), { persistent: true }); 16 | console.log(" [x] Sent '%s'", text); 17 | await channel.close(); 18 | } 19 | catch (err) { 20 | console.warn(err); 21 | } 22 | finally { 23 | await connection.close(); 24 | }; 25 | })(); 26 | -------------------------------------------------------------------------------- /examples/tutorials/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amqplib-tutorials", 3 | "version": "0.0.1", 4 | "description": "The RabbitMQ tutorials, ported to amqplib", 5 | "main": "send.js", 6 | "dependencies": { 7 | "amqplib": "../..", 8 | "uuid": "*" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": "", 14 | "author": "Michael Bridgen ", 15 | "license": "MPL 2.0" 16 | } 17 | -------------------------------------------------------------------------------- /examples/tutorials/receive.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const queue = 'hello'; 6 | 7 | (async () => { 8 | try { 9 | const connection = await amqp.connect('amqp://localhost'); 10 | const channel = await connection.createChannel(); 11 | 12 | process.once('SIGINT', async () => { 13 | await channel.close(); 14 | await connection.close(); 15 | }); 16 | 17 | await channel.assertQueue(queue, { durable: false }); 18 | await channel.consume(queue, (message) => { 19 | console.log(" [x] Received '%s'", message.content.toString()); 20 | }, { noAck: true }); 21 | 22 | console.log(' [*] Waiting for messages. To exit press CTRL+C'); 23 | } catch (err) { 24 | console.warn(err); 25 | } 26 | })(); 27 | -------------------------------------------------------------------------------- /examples/tutorials/receive_logs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const exchange = 'logs'; 6 | 7 | (async () => { 8 | try { 9 | const connection = await amqp.connect('amqp://localhost'); 10 | const channel = await connection.createChannel(); 11 | 12 | process.once('SIGINT', async () => { 13 | await channel.close(); 14 | await connection.close(); 15 | }); 16 | 17 | await channel.assertExchange(exchange, 'fanout', { durable: false }); 18 | const { queue } = await channel.assertQueue('', { exclusive: true }); 19 | await channel.bindQueue(queue, exchange, '') 20 | 21 | await channel.consume(queue, (message) => { 22 | if (message) console.log(" [x] '%s'", message.content.toString()); 23 | else console.warn(' [x] Consumer cancelled'); 24 | }, { noAck: true }); 25 | 26 | console.log(' [*] Waiting for logs. To exit press CTRL+C'); 27 | } catch (err) { 28 | console.warn(err); 29 | } 30 | })(); 31 | -------------------------------------------------------------------------------- /examples/tutorials/receive_logs_direct.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('../..'); 4 | const { basename } = require('path'); 5 | 6 | const exchange = 'direct_logs'; 7 | const bindingKeys = process.argv.slice(2); 8 | if (bindingKeys.length < 1) { 9 | console.warn('Usage: %s [info] [warning] [error]', basename(process.argv[1])); 10 | process.exit(1); 11 | } 12 | 13 | (async () => { 14 | try { 15 | const connection = await amqp.connect('amqp://localhost'); 16 | const channel = await connection.createChannel(); 17 | 18 | process.once('SIGINT', async () => { 19 | await channel.close(); 20 | await connection.close(); 21 | }); 22 | 23 | await channel.assertExchange(exchange, 'direct', { durable: false }); 24 | const { queue } = await channel.assertQueue('', { exclusive: true }); 25 | await Promise.all(bindingKeys.map(async (bindingKey) => { 26 | await channel.bindQueue(queue, exchange, bindingKey); 27 | })); 28 | 29 | await channel.consume(queue, (message) => { 30 | if (message) console.log(" [x] %s:'%s'", message.fields.routingKey, message.content.toString()); 31 | else console.warn(' [x] Consumer cancelled'); 32 | }, { noAck: true }); 33 | 34 | console.log(' [*] Waiting for logs. To exit press CTRL+C.'); 35 | } catch(err) { 36 | console.warn(err); 37 | } 38 | })(); 39 | -------------------------------------------------------------------------------- /examples/tutorials/receive_logs_topic.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('../..'); 4 | const { basename } = require('path'); 5 | 6 | const exchange = 'topic_logs'; 7 | const bindingKeys = process.argv.slice(2); 8 | if (bindingKeys.length < 1) { 9 | console.log('Usage: %s pattern [pattern...]', basename(process.argv[1])); 10 | process.exit(1); 11 | } 12 | 13 | (async () => { 14 | try { 15 | const connection = await amqp.connect('amqp://localhost'); 16 | const channel = await connection.createChannel(); 17 | 18 | process.once('SIGINT', async () => { 19 | await channel.close(); 20 | await connection.close(); 21 | }); 22 | 23 | await channel.assertExchange(exchange, 'topic', { durable: false }); 24 | const { queue } = await channel.assertQueue('', { exclusive: true }); 25 | await Promise.all(bindingKeys.map(async (bindingKey) => { 26 | await channel.bindQueue(queue, exchange, bindingKey); 27 | })); 28 | 29 | await channel.consume(queue, (message) => { 30 | if (message) console.log(" [x] %s:'%s'", message.fields.routingKey, message.content.toString()); 31 | else console.warn(' [x] Consumer cancelled'); 32 | }, { noAck: true }); 33 | 34 | console.log(' [*] Waiting for logs. To exit press CTRL+C.'); 35 | } 36 | catch (err) { 37 | console.warn(err); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /examples/tutorials/rpc_client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | const { basename } = require('path'); 5 | const { v4: uuid } = require('uuid'); 6 | 7 | const queue = 'rpc_queue'; 8 | 9 | const n = parseInt(process.argv[2], 10); 10 | if (isNaN(n)) { 11 | console.warn('Usage: %s number', basename(process.argv[1])); 12 | process.exit(1); 13 | } 14 | 15 | (async () => { 16 | let connection; 17 | try { 18 | connection = await amqp.connect('amqp://localhost'); 19 | const channel = await connection.createChannel(); 20 | const correlationId = uuid(); 21 | 22 | const requestFib = new Promise(async (resolve) => { 23 | const { queue: replyTo } = await channel.assertQueue('', { exclusive: true }); 24 | 25 | await channel.consume(replyTo, (message) => { 26 | if (!message) console.warn(' [x] Consumer cancelled'); 27 | else if (message.properties.correlationId === correlationId) { 28 | resolve(message.content.toString()); 29 | } 30 | }, { noAck: true }); 31 | 32 | await channel.assertQueue(queue, { durable: false }); 33 | console.log(' [x] Requesting fib(%d)', n); 34 | channel.sendToQueue(queue, Buffer.from(n.toString()), { 35 | correlationId, 36 | replyTo, 37 | }); 38 | }); 39 | 40 | const fibN = await requestFib; 41 | console.log(' [.] Got %d', fibN); 42 | } 43 | catch (err) { 44 | console.warn(err); 45 | } 46 | finally { 47 | if (connection) await connection.close(); 48 | }; 49 | })(); 50 | -------------------------------------------------------------------------------- /examples/tutorials/rpc_server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const queue = 'rpc_queue'; 6 | 7 | (async () => { 8 | try { 9 | const connection = await amqp.connect('amqp://localhost'); 10 | const channel = await connection.createChannel(); 11 | 12 | process.once('SIGINT', async () => { 13 | await channel.close(); 14 | await connection.close(); 15 | }); 16 | 17 | await channel.assertQueue(queue, { durable: false }); 18 | 19 | channel.prefetch(1); 20 | await channel.consume(queue, (message) => { 21 | const n = parseInt(message.content.toString(), 10); 22 | console.log(' [.] fib(%d)', n); 23 | const response = fib(n); 24 | channel.sendToQueue(message.properties.replyTo, Buffer.from(response.toString()), { 25 | correlationId: message.properties.correlationId 26 | }); 27 | channel.ack(message); 28 | }); 29 | 30 | console.log(' [x] Awaiting RPC requests. To exit press CTRL+C.'); 31 | } 32 | catch (err) { 33 | console.warn(err); 34 | } 35 | })(); 36 | 37 | function fib(n) { 38 | // Do it the ridiculous, but not most ridiculous, way. For better, 39 | // see http://nayuki.eigenstate.org/page/fast-fibonacci-algorithms 40 | let a = 0, b = 1; 41 | for (let i=0; i < n; i++) { 42 | let c = a + b; 43 | a = b; b = c; 44 | } 45 | return a; 46 | } 47 | -------------------------------------------------------------------------------- /examples/tutorials/send.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const queue = 'hello'; 6 | const text = 'Hello World!'; 7 | 8 | (async () => { 9 | let connection; 10 | try { 11 | connection = await amqp.connect('amqp://localhost'); 12 | const channel = await connection.createChannel(); 13 | 14 | await channel.assertQueue(queue, { durable: false }); 15 | 16 | // NB: `sentToQueue` and `publish` both return a boolean 17 | // indicating whether it's OK to send again straight away, or 18 | // (when `false`) that you should wait for the event `'drain'` 19 | // to fire before writing again. We're just doing the one write, 20 | // so we'll ignore it. 21 | channel.sendToQueue(queue, Buffer.from(text)); 22 | console.log(" [x] Sent '%s'", text); 23 | await channel.close(); 24 | } 25 | catch (err) { 26 | console.warn(err); 27 | } 28 | finally { 29 | if (connection) await connection.close(); 30 | }; 31 | })(); 32 | -------------------------------------------------------------------------------- /examples/tutorials/worker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Process tasks from the work queue 3 | 4 | const amqp = require('amqplib'); 5 | 6 | const queue = 'task_queue'; 7 | 8 | (async () => { 9 | try { 10 | const connection = await amqp.connect('amqp://localhost'); 11 | process.once('SIGINT', async () => { 12 | await connection.close(); 13 | }); 14 | 15 | const channel = await connection.createChannel(); 16 | await channel.assertQueue(queue, { durable: true }); 17 | 18 | channel.prefetch(1); 19 | await channel.consume(queue, (message) => { 20 | const text = message.content.toString(); 21 | console.log(" [x] Received '%s'", text); 22 | const seconds = text.split('.').length - 1; 23 | setTimeout(() => { 24 | console.log(" [x] Done"); 25 | channel.ack(message); 26 | }, seconds * 1000); 27 | }, { noAck: false }); 28 | 29 | console.log(" [*] Waiting for messages. To exit press CTRL+C"); 30 | } 31 | catch (err) { 32 | console.warn(err); 33 | } 34 | })(); 35 | -------------------------------------------------------------------------------- /examples/waitForConfirms.js: -------------------------------------------------------------------------------- 1 | const amqp = require('../'); 2 | 3 | (async () => { 4 | let connection; 5 | try { 6 | connection = await amqp.connect(); 7 | const channel = await connection.createConfirmChannel(); 8 | 9 | for (var i=0; i < 20; i++) { 10 | channel.publish('amq.topic', 'whatever', Buffer.from('blah')); 11 | }; 12 | 13 | await channel.waitForConfirms(); 14 | console.log('All messages done'); 15 | await channel.close(); 16 | } catch (err) { 17 | console.warn(err); 18 | } finally { 19 | if (connection) await connection.close(); 20 | } 21 | })(); 22 | -------------------------------------------------------------------------------- /lib/api_args.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | 'use strict'; 6 | 7 | /* 8 | The channel (promise) and callback APIs have similar signatures, and 9 | in particular, both need AMQP fields prepared from the same arguments 10 | and options. The arguments marshalling is done here. Each of the 11 | procedures below takes arguments and options (the latter in an object) 12 | particular to the operation it represents, and returns an object with 13 | fields for handing to the encoder. 14 | */ 15 | 16 | // A number of AMQP methods have a table-typed field called 17 | // `arguments`, that is intended to carry extension-specific 18 | // values. RabbitMQ uses this in a number of places; e.g., to specify 19 | // an 'alternate exchange'. 20 | // 21 | // Many of the methods in this API have an `options` argument, from 22 | // which I take both values that have a default in AMQP (e.g., 23 | // autoDelete in QueueDeclare) *and* values that are specific to 24 | // RabbitMQ (e.g., 'alternate-exchange'), which would normally be 25 | // supplied in `arguments`. So that extensions I don't support yet can 26 | // be used, I include `arguments` itself among the options. 27 | // 28 | // The upshot of this is that I often need to prepare an `arguments` 29 | // value that has any values passed in `options.arguments` as well as 30 | // any I've promoted to being options themselves. Since I don't want 31 | // to mutate anything passed in, the general pattern is to create a 32 | // fresh object with the `arguments` value given as its prototype; all 33 | // fields in the supplied value will be serialised, as well as any I 34 | // set on the fresh object. What I don't want to do, however, is set a 35 | // field to undefined by copying possibly missing field values, 36 | // because that will mask a value in the prototype. 37 | // 38 | // NB the `arguments` field already has a default value of `{}`, so 39 | // there's no need to explicitly default it unless I'm setting values. 40 | function setIfDefined(obj, prop, value) { 41 | if (value != undefined) obj[prop] = value; 42 | } 43 | 44 | var EMPTY_OPTIONS = Object.freeze({}); 45 | 46 | var Args = {}; 47 | 48 | Args.assertQueue = function(queue, options) { 49 | queue = queue || ''; 50 | options = options || EMPTY_OPTIONS; 51 | 52 | var argt = Object.create(options.arguments || null); 53 | setIfDefined(argt, 'x-expires', options.expires); 54 | setIfDefined(argt, 'x-message-ttl', options.messageTtl); 55 | setIfDefined(argt, 'x-dead-letter-exchange', 56 | options.deadLetterExchange); 57 | setIfDefined(argt, 'x-dead-letter-routing-key', 58 | options.deadLetterRoutingKey); 59 | setIfDefined(argt, 'x-max-length', options.maxLength); 60 | setIfDefined(argt, 'x-max-priority', options.maxPriority); 61 | setIfDefined(argt, 'x-overflow', options.overflow); 62 | setIfDefined(argt, 'x-queue-mode', options.queueMode); 63 | 64 | return { 65 | queue: queue, 66 | exclusive: !!options.exclusive, 67 | durable: (options.durable === undefined) ? true : options.durable, 68 | autoDelete: !!options.autoDelete, 69 | arguments: argt, 70 | passive: false, 71 | // deprecated but we have to include it 72 | ticket: 0, 73 | nowait: false 74 | }; 75 | }; 76 | 77 | Args.checkQueue = function(queue) { 78 | return { 79 | queue: queue, 80 | passive: true, // switch to "completely different" mode 81 | nowait: false, 82 | durable: true, autoDelete: false, exclusive: false, // ignored 83 | ticket: 0, 84 | }; 85 | }; 86 | 87 | Args.deleteQueue = function(queue, options) { 88 | options = options || EMPTY_OPTIONS; 89 | return { 90 | queue: queue, 91 | ifUnused: !!options.ifUnused, 92 | ifEmpty: !!options.ifEmpty, 93 | ticket: 0, nowait: false 94 | }; 95 | }; 96 | 97 | Args.purgeQueue = function(queue) { 98 | return { 99 | queue: queue, 100 | ticket: 0, nowait: false 101 | }; 102 | }; 103 | 104 | Args.bindQueue = function(queue, source, pattern, argt) { 105 | return { 106 | queue: queue, 107 | exchange: source, 108 | routingKey: pattern, 109 | arguments: argt, 110 | ticket: 0, nowait: false 111 | }; 112 | }; 113 | 114 | Args.unbindQueue = function(queue, source, pattern, argt) { 115 | return { 116 | queue: queue, 117 | exchange: source, 118 | routingKey: pattern, 119 | arguments: argt, 120 | ticket: 0, nowait: false 121 | }; 122 | }; 123 | 124 | Args.assertExchange = function(exchange, type, options) { 125 | options = options || EMPTY_OPTIONS; 126 | var argt = Object.create(options.arguments || null); 127 | setIfDefined(argt, 'alternate-exchange', options.alternateExchange); 128 | return { 129 | exchange: exchange, 130 | ticket: 0, 131 | type: type, 132 | passive: false, 133 | durable: (options.durable === undefined) ? true : options.durable, 134 | autoDelete: !!options.autoDelete, 135 | internal: !!options.internal, 136 | nowait: false, 137 | arguments: argt 138 | }; 139 | }; 140 | 141 | Args.checkExchange = function(exchange) { 142 | return { 143 | exchange: exchange, 144 | passive: true, // switch to 'may as well be another method' mode 145 | nowait: false, 146 | // ff are ignored 147 | durable: true, internal: false, type: '', autoDelete: false, 148 | ticket: 0 149 | }; 150 | }; 151 | 152 | Args.deleteExchange = function(exchange, options) { 153 | options = options || EMPTY_OPTIONS; 154 | return { 155 | exchange: exchange, 156 | ifUnused: !!options.ifUnused, 157 | ticket: 0, nowait: false 158 | }; 159 | }; 160 | 161 | Args.bindExchange = function(dest, source, pattern, argt) { 162 | return { 163 | source: source, 164 | destination: dest, 165 | routingKey: pattern, 166 | arguments: argt, 167 | ticket: 0, nowait: false 168 | }; 169 | }; 170 | 171 | Args.unbindExchange = function(dest, source, pattern, argt) { 172 | return { 173 | source: source, 174 | destination: dest, 175 | routingKey: pattern, 176 | arguments: argt, 177 | ticket: 0, nowait: false 178 | }; 179 | }; 180 | 181 | // It's convenient to construct the properties and the method fields 182 | // at the same time, since in the APIs, values for both can appear in 183 | // `options`. Since the property or mthod field names don't overlap, I 184 | // just return one big object that can be used for both purposes, and 185 | // the encoder will pick out what it wants. 186 | Args.publish = function(exchange, routingKey, options) { 187 | options = options || EMPTY_OPTIONS; 188 | 189 | // The CC and BCC fields expect an array of "longstr", which would 190 | // normally be buffer values in JavaScript; however, since a field 191 | // array (or table) cannot have shortstr values, the codec will 192 | // encode all strings as longstrs anyway. 193 | function convertCC(cc) { 194 | if (cc === undefined) { 195 | return undefined; 196 | } 197 | else if (Array.isArray(cc)) { 198 | return cc.map(String); 199 | } 200 | else return [String(cc)]; 201 | } 202 | 203 | var headers = Object.create(options.headers || null); 204 | setIfDefined(headers, 'CC', convertCC(options.CC)); 205 | setIfDefined(headers, 'BCC', convertCC(options.BCC)); 206 | 207 | var deliveryMode; // undefined will default to 1 (non-persistent) 208 | 209 | // Previously I overloaded deliveryMode be a boolean meaning 210 | // 'persistent or not'; better is to name this option for what it 211 | // is, but I need to have backwards compatibility for applications 212 | // that either supply a numeric or boolean value. 213 | if (options.persistent !== undefined) 214 | deliveryMode = (options.persistent) ? 2 : 1; 215 | else if (typeof options.deliveryMode === 'number') 216 | deliveryMode = options.deliveryMode; 217 | else if (options.deliveryMode) // is supplied and truthy 218 | deliveryMode = 2; 219 | 220 | var expiration = options.expiration; 221 | if (expiration !== undefined) expiration = expiration.toString(); 222 | 223 | return { 224 | // method fields 225 | exchange: exchange, 226 | routingKey: routingKey, 227 | mandatory: !!options.mandatory, 228 | immediate: false, // RabbitMQ doesn't implement this any more 229 | ticket: undefined, 230 | // properties 231 | contentType: options.contentType, 232 | contentEncoding: options.contentEncoding, 233 | headers: headers, 234 | deliveryMode: deliveryMode, 235 | priority: options.priority, 236 | correlationId: options.correlationId, 237 | replyTo: options.replyTo, 238 | expiration: expiration, 239 | messageId: options.messageId, 240 | timestamp: options.timestamp, 241 | type: options.type, 242 | userId: options.userId, 243 | appId: options.appId, 244 | clusterId: undefined 245 | }; 246 | }; 247 | 248 | Args.consume = function(queue, options) { 249 | options = options || EMPTY_OPTIONS; 250 | var argt = Object.create(options.arguments || null); 251 | setIfDefined(argt, 'x-priority', options.priority); 252 | return { 253 | ticket: 0, 254 | queue: queue, 255 | consumerTag: options.consumerTag || '', 256 | noLocal: !!options.noLocal, 257 | noAck: !!options.noAck, 258 | exclusive: !!options.exclusive, 259 | nowait: false, 260 | arguments: argt 261 | }; 262 | }; 263 | 264 | Args.cancel = function(consumerTag) { 265 | return { 266 | consumerTag: consumerTag, 267 | nowait: false 268 | }; 269 | }; 270 | 271 | Args.get = function(queue, options) { 272 | options = options || EMPTY_OPTIONS; 273 | return { 274 | ticket: 0, 275 | queue: queue, 276 | noAck: !!options.noAck 277 | }; 278 | }; 279 | 280 | Args.ack = function(tag, allUpTo) { 281 | return { 282 | deliveryTag: tag, 283 | multiple: !!allUpTo 284 | }; 285 | }; 286 | 287 | Args.nack = function(tag, allUpTo, requeue) { 288 | return { 289 | deliveryTag: tag, 290 | multiple: !!allUpTo, 291 | requeue: (requeue === undefined) ? true : requeue 292 | }; 293 | }; 294 | 295 | Args.reject = function(tag, requeue) { 296 | return { 297 | deliveryTag: tag, 298 | requeue: (requeue === undefined) ? true : requeue 299 | }; 300 | }; 301 | 302 | Args.prefetch = function(count, global) { 303 | return { 304 | prefetchCount: count || 0, 305 | prefetchSize: 0, 306 | global: !!global 307 | }; 308 | }; 309 | 310 | Args.recover = function() { 311 | return {requeue: true}; 312 | }; 313 | 314 | module.exports = Object.freeze(Args); 315 | -------------------------------------------------------------------------------- /lib/bitset.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * A bitset implementation, after that in java.util. Yes there 9 | * already exist such things, but none implement next{Clear|Set}Bit or 10 | * equivalent, and none involved me tooling about for an evening. 11 | */ 12 | class BitSet { 13 | /** 14 | * @param {number} [size] 15 | */ 16 | constructor(size) { 17 | if (size) { 18 | const numWords = Math.ceil(size / 32); 19 | this.words = new Array(numWords); 20 | } 21 | else { 22 | this.words = []; 23 | } 24 | this.wordsInUse = 0; // = number, not index 25 | } 26 | 27 | /** 28 | * @param {number} numWords 29 | */ 30 | ensureSize(numWords) { 31 | const wordsPresent = this.words.length; 32 | if (wordsPresent < numWords) { 33 | this.words = this.words.concat(new Array(numWords - wordsPresent)); 34 | } 35 | } 36 | 37 | /** 38 | * @param {number} bitIndex 39 | */ 40 | set(bitIndex) { 41 | const w = wordIndex(bitIndex); 42 | if (w >= this.wordsInUse) { 43 | this.ensureSize(w + 1); 44 | this.wordsInUse = w + 1; 45 | } 46 | const bit = 1 << bitIndex; 47 | this.words[w] |= bit; 48 | } 49 | 50 | /** 51 | * @param {number} bitIndex 52 | */ 53 | clear(bitIndex) { 54 | const w = wordIndex(bitIndex); 55 | if (w >= this.wordsInUse) return; 56 | const mask = ~(1 << bitIndex); 57 | this.words[w] &= mask; 58 | } 59 | 60 | /** 61 | * @param {number} bitIndex 62 | */ 63 | get(bitIndex) { 64 | const w = wordIndex(bitIndex); 65 | if (w >= this.wordsInUse) return false; // >= since index vs size 66 | const bit = 1 << bitIndex; 67 | return !!(this.words[w] & bit); 68 | } 69 | 70 | /** 71 | * Give the next bit that is set on or after fromIndex, or -1 if no such bit 72 | * 73 | * @param {number} fromIndex 74 | */ 75 | nextSetBit(fromIndex) { 76 | let w = wordIndex(fromIndex); 77 | if (w >= this.wordsInUse) return -1; 78 | 79 | // the right-hand side is shifted to only test the bits of the first 80 | // word that are > fromIndex 81 | let word = this.words[w] & (0xffffffff << fromIndex); 82 | while (true) { 83 | if (word) return (w * 32) + trailingZeros(word); 84 | w++; 85 | if (w === this.wordsInUse) return -1; 86 | word = this.words[w]; 87 | } 88 | } 89 | 90 | /** 91 | * @param {number} fromIndex 92 | */ 93 | nextClearBit(fromIndex) { 94 | let w = wordIndex(fromIndex); 95 | if (w >= this.wordsInUse) return fromIndex; 96 | 97 | let word = ~(this.words[w]) & (0xffffffff << fromIndex); 98 | while (true) { 99 | if (word) return (w * 32) + trailingZeros(word); 100 | w++; 101 | if (w == this.wordsInUse) return w * 32; 102 | word = ~(this.words[w]); 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * @param {number} bitIndex 109 | */ 110 | function wordIndex(bitIndex) { 111 | return Math.floor(bitIndex / 32); 112 | } 113 | 114 | /** 115 | * @param {number} i 116 | */ 117 | function trailingZeros(i) { 118 | // From Hacker's Delight, via JDK. Probably far less effective here, 119 | // since bit ops are not necessarily the quick way to do things in 120 | // JS. 121 | if (i === 0) return 32; 122 | let y, n = 31; 123 | y = i << 16; if (y != 0) { n = n -16; i = y; } 124 | y = i << 8; if (y != 0) { n = n - 8; i = y; } 125 | y = i << 4; if (y != 0) { n = n - 4; i = y; } 126 | y = i << 2; if (y != 0) { n = n - 2; i = y; } 127 | return n - ((i << 1) >>> 31); 128 | } 129 | 130 | module.exports.BitSet = BitSet; 131 | -------------------------------------------------------------------------------- /lib/callback_model.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | 'use strict'; 6 | 7 | var defs = require('./defs'); 8 | var EventEmitter = require('events'); 9 | var BaseChannel = require('./channel').BaseChannel; 10 | var acceptMessage = require('./channel').acceptMessage; 11 | var Args = require('./api_args'); 12 | 13 | class CallbackModel extends EventEmitter { 14 | constructor (connection) { 15 | super(); 16 | this.connection = connection; 17 | var self = this; 18 | ['error', 'close', 'blocked', 'unblocked'].forEach(function (ev) { 19 | connection.on(ev, self.emit.bind(self, ev)); 20 | }); 21 | } 22 | 23 | close (cb) { 24 | this.connection.close(cb); 25 | } 26 | 27 | updateSecret(newSecret, reason, cb) { 28 | this.connection._updateSecret(newSecret, reason, cb); 29 | } 30 | 31 | createChannel (options, cb) { 32 | if (arguments.length === 1) { 33 | cb = options; 34 | options = undefined; 35 | } 36 | var ch = new Channel(this.connection); 37 | ch.setOptions(options); 38 | ch.open(function (err, ok) { 39 | if (err === null) 40 | cb && cb(null, ch); 41 | else 42 | cb && cb(err); 43 | }); 44 | return ch; 45 | } 46 | 47 | createConfirmChannel (options, cb) { 48 | if (arguments.length === 1) { 49 | cb = options; 50 | options = undefined; 51 | } 52 | var ch = new ConfirmChannel(this.connection); 53 | ch.setOptions(options); 54 | ch.open(function (err) { 55 | if (err !== null) 56 | return cb && cb(err); 57 | else { 58 | ch.rpc(defs.ConfirmSelect, { nowait: false }, 59 | defs.ConfirmSelectOk, function (err, _ok) { 60 | if (err !== null) 61 | return cb && cb(err); 62 | else 63 | cb && cb(null, ch); 64 | }); 65 | } 66 | }); 67 | return ch; 68 | } 69 | } 70 | 71 | class Channel extends BaseChannel { 72 | constructor (connection) { 73 | super(connection); 74 | this.on('delivery', this.handleDelivery.bind(this)); 75 | this.on('cancel', this.handleCancel.bind(this)); 76 | } 77 | 78 | // This encodes straight-forward RPC: no side-effects and return the 79 | // fields from the server response. It wraps the callback given it, so 80 | // the calling method argument can be passed as-is. For anything that 81 | // needs to have side-effects, or needs to change the server response, 82 | // use `#_rpc(...)` and remember to dereference `.fields` of the 83 | // server response. 84 | rpc (method, fields, expect, cb0) { 85 | var cb = callbackWrapper(this, cb0); 86 | this._rpc(method, fields, expect, function (err, ok) { 87 | cb(err, ok && ok.fields); // in case of an error, ok will be 88 | 89 | // undefined 90 | }); 91 | return this; 92 | } 93 | 94 | // === Public API === 95 | open (cb) { 96 | try { this.allocate(); } 97 | catch (e) { return cb(e); } 98 | 99 | return this.rpc(defs.ChannelOpen, { outOfBand: "" }, 100 | defs.ChannelOpenOk, cb); 101 | } 102 | 103 | close (cb) { 104 | return this.closeBecause("Goodbye", defs.constants.REPLY_SUCCESS, 105 | function () { cb && cb(null); }); 106 | } 107 | 108 | assertQueue (queue, options, cb) { 109 | return this.rpc(defs.QueueDeclare, 110 | Args.assertQueue(queue, options), 111 | defs.QueueDeclareOk, cb); 112 | } 113 | 114 | checkQueue (queue, cb) { 115 | return this.rpc(defs.QueueDeclare, 116 | Args.checkQueue(queue), 117 | defs.QueueDeclareOk, cb); 118 | } 119 | 120 | deleteQueue (queue, options, cb) { 121 | return this.rpc(defs.QueueDelete, 122 | Args.deleteQueue(queue, options), 123 | defs.QueueDeleteOk, cb); 124 | } 125 | 126 | purgeQueue (queue, cb) { 127 | return this.rpc(defs.QueuePurge, 128 | Args.purgeQueue(queue), 129 | defs.QueuePurgeOk, cb); 130 | } 131 | 132 | bindQueue (queue, source, pattern, argt, cb) { 133 | return this.rpc(defs.QueueBind, 134 | Args.bindQueue(queue, source, pattern, argt), 135 | defs.QueueBindOk, cb); 136 | } 137 | 138 | unbindQueue (queue, source, pattern, argt, cb) { 139 | return this.rpc(defs.QueueUnbind, 140 | Args.unbindQueue(queue, source, pattern, argt), 141 | defs.QueueUnbindOk, cb); 142 | } 143 | 144 | assertExchange (ex, type, options, cb0) { 145 | var cb = callbackWrapper(this, cb0); 146 | this._rpc(defs.ExchangeDeclare, 147 | Args.assertExchange(ex, type, options), 148 | defs.ExchangeDeclareOk, 149 | function (e, _) { cb(e, { exchange: ex }); }); 150 | return this; 151 | } 152 | 153 | checkExchange (exchange, cb) { 154 | return this.rpc(defs.ExchangeDeclare, 155 | Args.checkExchange(exchange), 156 | defs.ExchangeDeclareOk, cb); 157 | } 158 | 159 | deleteExchange (exchange, options, cb) { 160 | return this.rpc(defs.ExchangeDelete, 161 | Args.deleteExchange(exchange, options), 162 | defs.ExchangeDeleteOk, cb); 163 | } 164 | 165 | bindExchange (dest, source, pattern, argt, cb) { 166 | return this.rpc(defs.ExchangeBind, 167 | Args.bindExchange(dest, source, pattern, argt), 168 | defs.ExchangeBindOk, cb); 169 | } 170 | 171 | unbindExchange (dest, source, pattern, argt, cb) { 172 | return this.rpc(defs.ExchangeUnbind, 173 | Args.unbindExchange(dest, source, pattern, argt), 174 | defs.ExchangeUnbindOk, cb); 175 | } 176 | 177 | publish (exchange, routingKey, content, options) { 178 | var fieldsAndProps = Args.publish(exchange, routingKey, options); 179 | return this.sendMessage(fieldsAndProps, fieldsAndProps, content); 180 | } 181 | 182 | sendToQueue (queue, content, options) { 183 | return this.publish('', queue, content, options); 184 | } 185 | 186 | consume (queue, callback, options, cb0) { 187 | var cb = callbackWrapper(this, cb0); 188 | var fields = Args.consume(queue, options); 189 | var self = this; 190 | this._rpc( 191 | defs.BasicConsume, fields, defs.BasicConsumeOk, 192 | function (err, ok) { 193 | if (err === null) { 194 | self.registerConsumer(ok.fields.consumerTag, callback); 195 | cb(null, ok.fields); 196 | } 197 | else 198 | cb(err); 199 | }); 200 | return this; 201 | } 202 | 203 | cancel (consumerTag, cb0) { 204 | var cb = callbackWrapper(this, cb0); 205 | var self = this; 206 | this._rpc( 207 | defs.BasicCancel, Args.cancel(consumerTag), defs.BasicCancelOk, 208 | function (err, ok) { 209 | if (err === null) { 210 | self.unregisterConsumer(consumerTag); 211 | cb(null, ok.fields); 212 | } 213 | else 214 | cb(err); 215 | }); 216 | return this; 217 | } 218 | 219 | get (queue, options, cb0) { 220 | var self = this; 221 | var fields = Args.get(queue, options); 222 | var cb = callbackWrapper(this, cb0); 223 | this.sendOrEnqueue(defs.BasicGet, fields, function (err, f) { 224 | if (err === null) { 225 | if (f.id === defs.BasicGetEmpty) { 226 | cb(null, false); 227 | } 228 | else if (f.id === defs.BasicGetOk) { 229 | self.handleMessage = acceptMessage(function (m) { 230 | m.fields = f.fields; 231 | cb(null, m); 232 | }); 233 | } 234 | else { 235 | cb(new Error("Unexpected response to BasicGet: " + 236 | inspect(f))); 237 | } 238 | } 239 | }); 240 | return this; 241 | } 242 | 243 | ack (message, allUpTo) { 244 | this.sendImmediately( 245 | defs.BasicAck, Args.ack(message.fields.deliveryTag, allUpTo)); 246 | return this; 247 | } 248 | 249 | ackAll () { 250 | this.sendImmediately(defs.BasicAck, Args.ack(0, true)); 251 | return this; 252 | } 253 | 254 | nack (message, allUpTo, requeue) { 255 | this.sendImmediately( 256 | defs.BasicNack, 257 | Args.nack(message.fields.deliveryTag, allUpTo, requeue)); 258 | return this; 259 | } 260 | 261 | nackAll (requeue) { 262 | this.sendImmediately( 263 | defs.BasicNack, Args.nack(0, true, requeue)); 264 | return this; 265 | } 266 | 267 | reject (message, requeue) { 268 | this.sendImmediately( 269 | defs.BasicReject, 270 | Args.reject(message.fields.deliveryTag, requeue)); 271 | return this; 272 | } 273 | 274 | prefetch (count, global, cb) { 275 | return this.rpc(defs.BasicQos, 276 | Args.prefetch(count, global), 277 | defs.BasicQosOk, cb); 278 | } 279 | 280 | recover (cb) { 281 | return this.rpc(defs.BasicRecover, 282 | Args.recover(), 283 | defs.BasicRecoverOk, cb); 284 | } 285 | } 286 | 287 | 288 | // Wrap an RPC callback to make sure the callback is invoked with 289 | // either `(null, value)` or `(error)`, i.e., never two non-null 290 | // values. Also substitutes a stub if the callback is `undefined` or 291 | // otherwise falsey, for convenience in methods for which the callback 292 | // is optional (that is, most of them). 293 | function callbackWrapper(ch, cb) { 294 | return (cb) ? function(err, ok) { 295 | if (err === null) { 296 | cb(null, ok); 297 | } 298 | else cb(err); 299 | } : function() {}; 300 | } 301 | 302 | class ConfirmChannel extends Channel { 303 | publish (exchange, routingKey, 304 | content, options, cb) { 305 | this.pushConfirmCallback(cb); 306 | return Channel.prototype.publish.call( 307 | this, exchange, routingKey, content, options); 308 | } 309 | 310 | sendToQueue (queue, content, 311 | options, cb) { 312 | return this.publish('', queue, content, options, cb); 313 | } 314 | 315 | waitForConfirms (k) { 316 | var awaiting = []; 317 | var unconfirmed = this.unconfirmed; 318 | unconfirmed.forEach(function (val, index) { 319 | if (val === null) 320 | ; // already confirmed 321 | else { 322 | var confirmed = new Promise(function (resolve, reject) { 323 | unconfirmed[index] = function (err) { 324 | if (val) 325 | val(err); 326 | if (err === null) 327 | resolve(); 328 | else 329 | reject(err); 330 | }; 331 | }); 332 | awaiting.push(confirmed); 333 | } 334 | }); 335 | return Promise.all(awaiting).then(function () { k(); }, 336 | function (err) { k(err); }); 337 | } 338 | } 339 | 340 | module.exports.CallbackModel = CallbackModel; 341 | module.exports.Channel = Channel; 342 | module.exports.ConfirmChannel = ConfirmChannel; 343 | -------------------------------------------------------------------------------- /lib/channel_model.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | 'use strict'; 6 | 7 | const EventEmitter = require('events'); 8 | const promisify = require('util').promisify; 9 | const defs = require('./defs'); 10 | const {BaseChannel} = require('./channel'); 11 | const {acceptMessage} = require('./channel'); 12 | const Args = require('./api_args'); 13 | const {inspect} = require('./format'); 14 | 15 | class ChannelModel extends EventEmitter { 16 | constructor(connection) { 17 | super(); 18 | this.connection = connection; 19 | 20 | ['error', 'close', 'blocked', 'unblocked'].forEach(ev => { 21 | connection.on(ev, this.emit.bind(this, ev)); 22 | }); 23 | } 24 | 25 | close() { 26 | return promisify(this.connection.close.bind(this.connection))(); 27 | } 28 | 29 | updateSecret(newSecret, reason) { 30 | return promisify(this.connection._updateSecret.bind(this.connection))(newSecret, reason); 31 | } 32 | 33 | async createChannel(options) { 34 | const channel = new Channel(this.connection); 35 | channel.setOptions(options); 36 | await channel.open(); 37 | return channel; 38 | } 39 | 40 | async createConfirmChannel(options) { 41 | const channel = new ConfirmChannel(this.connection); 42 | channel.setOptions(options); 43 | await channel.open(); 44 | await channel.rpc(defs.ConfirmSelect, {nowait: false}, defs.ConfirmSelectOk); 45 | return channel; 46 | } 47 | } 48 | 49 | // Channels 50 | 51 | class Channel extends BaseChannel { 52 | constructor(connection) { 53 | super(connection); 54 | this.on('delivery', this.handleDelivery.bind(this)); 55 | this.on('cancel', this.handleCancel.bind(this)); 56 | } 57 | 58 | // An RPC that returns a 'proper' promise, which resolves to just the 59 | // response's fields; this is intended to be suitable for implementing 60 | // API procedures. 61 | async rpc(method, fields, expect) { 62 | const f = await promisify(cb => { 63 | return this._rpc(method, fields, expect, cb); 64 | })(); 65 | 66 | return f.fields; 67 | } 68 | 69 | // Do the remarkably simple channel open handshake 70 | async open() { 71 | const ch = await this.allocate.bind(this)(); 72 | return ch.rpc(defs.ChannelOpen, {outOfBand: ""}, 73 | defs.ChannelOpenOk); 74 | } 75 | 76 | close() { 77 | return promisify(cb => { 78 | return this.closeBecause("Goodbye", defs.constants.REPLY_SUCCESS, 79 | cb); 80 | })(); 81 | } 82 | 83 | // === Public API, declaring queues and stuff === 84 | 85 | assertQueue(queue, options) { 86 | return this.rpc(defs.QueueDeclare, 87 | Args.assertQueue(queue, options), 88 | defs.QueueDeclareOk); 89 | } 90 | 91 | checkQueue(queue) { 92 | return this.rpc(defs.QueueDeclare, 93 | Args.checkQueue(queue), 94 | defs.QueueDeclareOk); 95 | } 96 | 97 | deleteQueue(queue, options) { 98 | return this.rpc(defs.QueueDelete, 99 | Args.deleteQueue(queue, options), 100 | defs.QueueDeleteOk); 101 | } 102 | 103 | purgeQueue(queue) { 104 | return this.rpc(defs.QueuePurge, 105 | Args.purgeQueue(queue), 106 | defs.QueuePurgeOk); 107 | } 108 | 109 | bindQueue(queue, source, pattern, argt) { 110 | return this.rpc(defs.QueueBind, 111 | Args.bindQueue(queue, source, pattern, argt), 112 | defs.QueueBindOk); 113 | } 114 | 115 | unbindQueue(queue, source, pattern, argt) { 116 | return this.rpc(defs.QueueUnbind, 117 | Args.unbindQueue(queue, source, pattern, argt), 118 | defs.QueueUnbindOk); 119 | } 120 | 121 | assertExchange(exchange, type, options) { 122 | // The server reply is an empty set of fields, but it's convenient 123 | // to have the exchange name handed to the continuation. 124 | return this.rpc(defs.ExchangeDeclare, 125 | Args.assertExchange(exchange, type, options), 126 | defs.ExchangeDeclareOk) 127 | .then(_ok => { return { exchange }; }); 128 | } 129 | 130 | checkExchange(exchange) { 131 | return this.rpc(defs.ExchangeDeclare, 132 | Args.checkExchange(exchange), 133 | defs.ExchangeDeclareOk); 134 | } 135 | 136 | deleteExchange(name, options) { 137 | return this.rpc(defs.ExchangeDelete, 138 | Args.deleteExchange(name, options), 139 | defs.ExchangeDeleteOk); 140 | } 141 | 142 | bindExchange(dest, source, pattern, argt) { 143 | return this.rpc(defs.ExchangeBind, 144 | Args.bindExchange(dest, source, pattern, argt), 145 | defs.ExchangeBindOk); 146 | } 147 | 148 | unbindExchange(dest, source, pattern, argt) { 149 | return this.rpc(defs.ExchangeUnbind, 150 | Args.unbindExchange(dest, source, pattern, argt), 151 | defs.ExchangeUnbindOk); 152 | } 153 | 154 | // Working with messages 155 | 156 | publish(exchange, routingKey, content, options) { 157 | const fieldsAndProps = Args.publish(exchange, routingKey, options); 158 | return this.sendMessage(fieldsAndProps, fieldsAndProps, content); 159 | } 160 | 161 | sendToQueue(queue, content, options) { 162 | return this.publish('', queue, content, options); 163 | } 164 | 165 | consume(queue, callback, options) { 166 | // NB we want the callback to be run synchronously, so that we've 167 | // registered the consumerTag before any messages can arrive. 168 | const fields = Args.consume(queue, options); 169 | return new Promise((resolve, reject) => { 170 | this._rpc(defs.BasicConsume, fields, defs.BasicConsumeOk, (err, ok) => { 171 | if (err) return reject(err); 172 | this.registerConsumer(ok.fields.consumerTag, callback); 173 | resolve(ok.fields); 174 | }); 175 | }); 176 | } 177 | 178 | async cancel(consumerTag) { 179 | const ok = await promisify(cb => { 180 | this._rpc(defs.BasicCancel, Args.cancel(consumerTag), 181 | defs.BasicCancelOk, 182 | cb); 183 | })() 184 | .then(ok => { 185 | this.unregisterConsumer(consumerTag); 186 | return ok.fields; 187 | }); 188 | } 189 | 190 | get(queue, options) { 191 | const fields = Args.get(queue, options); 192 | return new Promise((resolve, reject) => { 193 | this.sendOrEnqueue(defs.BasicGet, fields, (err, f) => { 194 | if (err) return reject(err); 195 | if (f.id === defs.BasicGetEmpty) { 196 | return resolve(false); 197 | } 198 | else if (f.id === defs.BasicGetOk) { 199 | const fields = f.fields; 200 | this.handleMessage = acceptMessage(m => { 201 | m.fields = fields; 202 | resolve(m); 203 | }); 204 | } 205 | else { 206 | reject(new Error(`Unexpected response to BasicGet: ${inspect(f)}`)); 207 | } 208 | }); 209 | }); 210 | } 211 | 212 | ack(message, allUpTo) { 213 | this.sendImmediately( 214 | defs.BasicAck, 215 | Args.ack(message.fields.deliveryTag, allUpTo)); 216 | } 217 | 218 | ackAll() { 219 | this.sendImmediately(defs.BasicAck, Args.ack(0, true)); 220 | } 221 | 222 | nack(message, allUpTo, requeue) { 223 | this.sendImmediately( 224 | defs.BasicNack, 225 | Args.nack(message.fields.deliveryTag, allUpTo, requeue)); 226 | } 227 | 228 | nackAll(requeue) { 229 | this.sendImmediately(defs.BasicNack, 230 | Args.nack(0, true, requeue)); 231 | } 232 | 233 | // `Basic.Nack` is not available in older RabbitMQ versions (or in the 234 | // AMQP specification), so you have to use the one-at-a-time 235 | // `Basic.Reject`. This is otherwise synonymous with 236 | // `#nack(message, false, requeue)`. 237 | reject(message, requeue) { 238 | this.sendImmediately( 239 | defs.BasicReject, 240 | Args.reject(message.fields.deliveryTag, requeue)); 241 | } 242 | 243 | recover() { 244 | return this.rpc(defs.BasicRecover, 245 | Args.recover(), 246 | defs.BasicRecoverOk); 247 | } 248 | 249 | qos(count, global) { 250 | return this.rpc(defs.BasicQos, 251 | Args.prefetch(count, global), 252 | defs.BasicQosOk); 253 | } 254 | } 255 | 256 | // There are more options in AMQP than exposed here; RabbitMQ only 257 | // implements prefetch based on message count, and only for individual 258 | // channels or consumers. RabbitMQ v3.3.0 and after treat prefetch 259 | // (without `global` set) as per-consumer (for consumers following), 260 | // and prefetch with `global` set as per-channel. 261 | Channel.prototype.prefetch = Channel.prototype.qos 262 | 263 | // Confirm channel. This is a channel with confirms 'switched on', 264 | // meaning sent messages will provoke a responding 'ack' or 'nack' 265 | // from the server. The upshot of this is that `publish` and 266 | // `sendToQueue` both take a callback, which will be called either 267 | // with `null` as its argument to signify 'ack', or an exception as 268 | // its argument to signify 'nack'. 269 | 270 | class ConfirmChannel extends Channel { 271 | publish(exchange, routingKey, content, options, cb) { 272 | this.pushConfirmCallback(cb); 273 | return super.publish(exchange, routingKey, content, options); 274 | } 275 | 276 | sendToQueue(queue, content, options, cb) { 277 | return this.publish('', queue, content, options, cb); 278 | } 279 | 280 | waitForConfirms() { 281 | const awaiting = []; 282 | const unconfirmed = this.unconfirmed; 283 | unconfirmed.forEach((val, index) => { 284 | if (val !== null) { 285 | const confirmed = new Promise((resolve, reject) => { 286 | unconfirmed[index] = err => { 287 | if (val) val(err); 288 | if (err === null) resolve(); 289 | else reject(err); 290 | }; 291 | }); 292 | awaiting.push(confirmed); 293 | } 294 | }); 295 | // Channel closed 296 | if (!this.pending) { 297 | var cb; 298 | while (cb = this.unconfirmed.shift()) { 299 | if (cb) cb(new Error('channel closed')); 300 | } 301 | } 302 | return Promise.all(awaiting); 303 | } 304 | } 305 | 306 | module.exports.ConfirmChannel = ConfirmChannel; 307 | module.exports.Channel = Channel; 308 | module.exports.ChannelModel = ChannelModel; 309 | -------------------------------------------------------------------------------- /lib/codec.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | /* 6 | 7 | The AMQP 0-9-1 is a mess when it comes to the types that can be 8 | encoded on the wire. 9 | 10 | There are four encoding schemes, and three overlapping sets of types: 11 | frames, methods, (field-)tables, and properties. 12 | 13 | Each *frame type* has a set layout in which values of given types are 14 | concatenated along with sections of "raw binary" data. 15 | 16 | In frames there are `shortstr`s, that is length-prefixed strings of 17 | UTF8 chars, 8 bit unsigned integers (called `octet`), unsigned 16 bit 18 | integers (called `short` or `short-uint`), unsigned 32 bit integers 19 | (called `long` or `long-uint`), unsigned 64 bit integers (called 20 | `longlong` or `longlong-uint`), and flags (called `bit`). 21 | 22 | Methods are encoded as a frame giving a method ID and a sequence of 23 | arguments of known types. The encoded method argument values are 24 | concatenated (with some fun complications around "packing" consecutive 25 | bit values into bytes). 26 | 27 | Along with the types given in frames, method arguments may be long 28 | byte strings (`longstr`, not required to be UTF8) or 64 bit unsigned 29 | integers to be interpreted as timestamps (yeah I don't know why 30 | either), or arbitrary sets of key-value pairs (called `field-table`). 31 | 32 | Inside a field table the keys are `shortstr` and the values are 33 | prefixed with a byte tag giving the type. The types are any of the 34 | above except for bits (which are replaced by byte-wide `bool`), along 35 | with a NULL value `void`, a special fixed-precision number encoding 36 | (`decimal`), IEEE754 `float`s and `double`s, signed integers, 37 | `field-array` (a sequence of tagged values), and nested field-tables. 38 | 39 | RabbitMQ and QPid use a subset of the field-table types, and different 40 | value tags, established before the AMQP 0-9-1 specification was 41 | published. So far as I know, no-one uses the types and tags as 42 | published. http://www.rabbitmq.com/amqp-0-9-1-errata.html gives the 43 | list of field-table types. 44 | 45 | Lastly, there are (sets of) properties, only one of which is given in 46 | AMQP 0-9-1: `BasicProperties`. These are almost the same as methods, 47 | except that they appear in content header frames, which include a 48 | content size, and they carry a set of flags indicating which 49 | properties are present. This scheme can save ones of bytes per message 50 | (messages which take a minimum of three frames each to send). 51 | 52 | */ 53 | 54 | 'use strict'; 55 | 56 | var ints = require('buffer-more-ints'); 57 | 58 | // JavaScript uses only doubles so what I'm testing for is whether 59 | // it's *better* to encode a number as a float or double. This really 60 | // just amounts to testing whether there's a fractional part to the 61 | // number, except that see below. NB I don't use bitwise operations to 62 | // do this 'efficiently' -- it would mask the number to 32 bits. 63 | // 64 | // At 2^50, doubles don't have sufficient precision to distinguish 65 | // between floating point and integer numbers (`Math.pow(2, 50) + 0.1 66 | // === Math.pow(2, 50)` (and, above 2^53, doubles cannot represent all 67 | // integers (`Math.pow(2, 53) + 1 === Math.pow(2, 53)`)). Hence 68 | // anything with a magnitude at or above 2^50 may as well be encoded 69 | // as a 64-bit integer. Except that only signed integers are supported 70 | // by RabbitMQ, so anything above 2^63 - 1 must be a double. 71 | function isFloatingPoint(n) { 72 | return n >= 0x8000000000000000 || 73 | (Math.abs(n) < 0x4000000000000 74 | && Math.floor(n) !== n); 75 | } 76 | 77 | function encodeTable(buffer, val, offset) { 78 | var start = offset; 79 | offset += 4; // leave room for the table length 80 | for (var key in val) { 81 | if (val[key] !== undefined) { 82 | var len = Buffer.byteLength(key); 83 | buffer.writeUInt8(len, offset); offset++; 84 | buffer.write(key, offset, 'utf8'); offset += len; 85 | offset += encodeFieldValue(buffer, val[key], offset); 86 | } 87 | } 88 | var size = offset - start; 89 | buffer.writeUInt32BE(size - 4, start); 90 | return size; 91 | } 92 | 93 | function encodeArray(buffer, val, offset) { 94 | var start = offset; 95 | offset += 4; 96 | for (var i=0, num=val.length; i < num; i++) { 97 | offset += encodeFieldValue(buffer, val[i], offset); 98 | } 99 | var size = offset - start; 100 | buffer.writeUInt32BE(size - 4, start); 101 | return size; 102 | } 103 | 104 | function encodeFieldValue(buffer, value, offset) { 105 | var start = offset; 106 | var type = typeof value, val = value; 107 | // A trapdoor for specifying a type, e.g., timestamp 108 | if (value && type === 'object' && value.hasOwnProperty('!')) { 109 | val = value.value; 110 | type = value['!']; 111 | } 112 | 113 | // If it's a JS number, we'll have to guess what type to encode it 114 | // as. 115 | if (type == 'number') { 116 | // Making assumptions about the kind of number (floating point 117 | // v integer, signed, unsigned, size) desired is dangerous in 118 | // general; however, in practice RabbitMQ uses only 119 | // longstrings and unsigned integers in its arguments, and 120 | // other clients generally conflate number types anyway. So 121 | // the only distinction we care about is floating point vs 122 | // integers, preferring integers since those can be promoted 123 | // if necessary. If floating point is required, we may as well 124 | // use double precision. 125 | if (isFloatingPoint(val)) { 126 | type = 'double'; 127 | } 128 | else { // only signed values are used in tables by 129 | // RabbitMQ. It *used* to (< v3.3.0) treat the byte 'b' 130 | // type as unsigned, but most clients (and the spec) 131 | // think it's signed, and now RabbitMQ does too. 132 | if (val < 128 && val >= -128) { 133 | type = 'byte'; 134 | } 135 | else if (val >= -0x8000 && val < 0x8000) { 136 | type = 'short' 137 | } 138 | else if (val >= -0x80000000 && val < 0x80000000) { 139 | type = 'int'; 140 | } 141 | else { 142 | type = 'long'; 143 | } 144 | } 145 | } 146 | 147 | function tag(t) { buffer.write(t, offset); offset++; } 148 | 149 | switch (type) { 150 | case 'string': // no shortstr in field tables 151 | var len = Buffer.byteLength(val, 'utf8'); 152 | tag('S'); 153 | buffer.writeUInt32BE(len, offset); offset += 4; 154 | buffer.write(val, offset, 'utf8'); offset += len; 155 | break; 156 | case 'object': 157 | if (val === null) { 158 | tag('V'); 159 | } 160 | else if (Array.isArray(val)) { 161 | tag('A'); 162 | offset += encodeArray(buffer, val, offset); 163 | } 164 | else if (Buffer.isBuffer(val)) { 165 | tag('x'); 166 | buffer.writeUInt32BE(val.length, offset); offset += 4; 167 | val.copy(buffer, offset); offset += val.length; 168 | } 169 | else { 170 | tag('F'); 171 | offset += encodeTable(buffer, val, offset); 172 | } 173 | break; 174 | case 'boolean': 175 | tag('t'); 176 | buffer.writeUInt8((val) ? 1 : 0, offset); offset++; 177 | break; 178 | // These are the types that are either guessed above, or 179 | // explicitly given using the {'!': type} notation. 180 | case 'double': 181 | case 'float64': 182 | tag('d'); 183 | buffer.writeDoubleBE(val, offset); 184 | offset += 8; 185 | break; 186 | case 'byte': 187 | case 'int8': 188 | tag('b'); 189 | buffer.writeInt8(val, offset); offset++; 190 | break; 191 | case 'unsignedbyte': 192 | case 'uint8': 193 | tag('B'); 194 | buffer.writeUInt8(val, offset); offset++; 195 | break; 196 | case 'short': 197 | case 'int16': 198 | tag('s'); 199 | buffer.writeInt16BE(val, offset); offset += 2; 200 | break; 201 | case 'unsignedshort': 202 | case 'uint16': 203 | tag('u'); 204 | buffer.writeUInt16BE(val, offset); offset += 2; 205 | break; 206 | case 'int': 207 | case 'int32': 208 | tag('I'); 209 | buffer.writeInt32BE(val, offset); offset += 4; 210 | break; 211 | case 'unsignedint': 212 | case 'uint32': 213 | tag('i'); 214 | buffer.writeUInt32BE(val, offset); offset += 4; 215 | break; 216 | case 'long': 217 | case 'int64': 218 | tag('l'); 219 | ints.writeInt64BE(buffer, val, offset); offset += 8; 220 | break; 221 | 222 | // Now for exotic types, those can _only_ be denoted by using 223 | // `{'!': type, value: val} 224 | case 'timestamp': 225 | tag('T'); 226 | ints.writeUInt64BE(buffer, val, offset); offset += 8; 227 | break; 228 | case 'float': 229 | tag('f'); 230 | buffer.writeFloatBE(val, offset); offset += 4; 231 | break; 232 | case 'decimal': 233 | tag('D'); 234 | if (val.hasOwnProperty('places') && val.hasOwnProperty('digits') 235 | && val.places >= 0 && val.places < 256) { 236 | buffer[offset] = val.places; offset++; 237 | buffer.writeUInt32BE(val.digits, offset); offset += 4; 238 | } 239 | else throw new TypeError( 240 | "Decimal value must be {'places': 0..255, 'digits': uint32}, " + 241 | "got " + JSON.stringify(val)); 242 | break; 243 | default: 244 | throw new TypeError('Unknown type to encode: ' + type); 245 | } 246 | return offset - start; 247 | } 248 | 249 | // Assume we're given a slice of the buffer that contains just the 250 | // fields. 251 | function decodeFields(slice) { 252 | var fields = {}, offset = 0, size = slice.length; 253 | var len, key, val; 254 | 255 | function decodeFieldValue() { 256 | var tag = String.fromCharCode(slice[offset]); offset++; 257 | switch (tag) { 258 | case 'b': 259 | val = slice.readInt8(offset); offset++; 260 | break; 261 | case 'B': 262 | val = slice.readUInt8(offset); offset++; 263 | break; 264 | case 'S': 265 | len = slice.readUInt32BE(offset); offset += 4; 266 | val = slice.toString('utf8', offset, offset + len); 267 | offset += len; 268 | break; 269 | case 'I': 270 | val = slice.readInt32BE(offset); offset += 4; 271 | break; 272 | case 'i': 273 | val = slice.readUInt32BE(offset); offset += 4; 274 | break; 275 | case 'D': // only positive decimals, apparently. 276 | var places = slice[offset]; offset++; 277 | var digits = slice.readUInt32BE(offset); offset += 4; 278 | val = {'!': 'decimal', value: {places: places, digits: digits}}; 279 | break; 280 | case 'T': 281 | val = ints.readUInt64BE(slice, offset); offset += 8; 282 | val = {'!': 'timestamp', value: val}; 283 | break; 284 | case 'F': 285 | len = slice.readUInt32BE(offset); offset += 4; 286 | val = decodeFields(slice.subarray(offset, offset + len)); 287 | offset += len; 288 | break; 289 | case 'A': 290 | len = slice.readUInt32BE(offset); offset += 4; 291 | decodeArray(offset + len); 292 | // NB decodeArray will itself update offset and val 293 | break; 294 | case 'd': 295 | val = slice.readDoubleBE(offset); offset += 8; 296 | break; 297 | case 'f': 298 | val = slice.readFloatBE(offset); offset += 4; 299 | break; 300 | case 'l': 301 | val = ints.readInt64BE(slice, offset); offset += 8; 302 | break; 303 | case 's': 304 | val = slice.readInt16BE(offset); offset += 2; 305 | break; 306 | case 'u': 307 | val = slice.readUInt16BE(offset); offset += 2; 308 | break; 309 | case 't': 310 | val = slice[offset] != 0; offset++; 311 | break; 312 | case 'V': 313 | val = null; 314 | break; 315 | case 'x': 316 | len = slice.readUInt32BE(offset); offset += 4; 317 | val = slice.subarray(offset, offset + len); 318 | offset += len; 319 | break; 320 | default: 321 | throw new TypeError('Unexpected type tag "' + tag +'"'); 322 | } 323 | } 324 | 325 | function decodeArray(until) { 326 | var vals = []; 327 | while (offset < until) { 328 | decodeFieldValue(); 329 | vals.push(val); 330 | } 331 | val = vals; 332 | } 333 | 334 | while (offset < size) { 335 | len = slice.readUInt8(offset); offset++; 336 | key = slice.toString('utf8', offset, offset + len); 337 | offset += len; 338 | decodeFieldValue(); 339 | fields[key] = val; 340 | } 341 | return fields; 342 | } 343 | 344 | module.exports.encodeTable = encodeTable; 345 | module.exports.decodeFields = decodeFields; 346 | -------------------------------------------------------------------------------- /lib/connect.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | // General-purpose API for glueing everything together. 6 | 7 | 'use strict'; 8 | 9 | var URL = require('url-parse'); 10 | var QS = require('querystring'); 11 | var Connection = require('./connection').Connection; 12 | var fmt = require('util').format; 13 | var credentials = require('./credentials'); 14 | 15 | function copyInto(obj, target) { 16 | var keys = Object.keys(obj); 17 | var i = keys.length; 18 | while (i--) { 19 | var k = keys[i]; 20 | target[k] = obj[k]; 21 | } 22 | return target; 23 | } 24 | 25 | // Adapted from util._extend, which is too fringe to use. 26 | function clone(obj) { 27 | return copyInto(obj, {}); 28 | } 29 | 30 | var CLIENT_PROPERTIES = { 31 | "product": "amqplib", 32 | "version": require('../package.json').version, 33 | "platform": fmt('Node.JS %s', process.version), 34 | "information": "https://amqp-node.github.io/amqplib/", 35 | "capabilities": { 36 | "publisher_confirms": true, 37 | "exchange_exchange_bindings": true, 38 | "basic.nack": true, 39 | "consumer_cancel_notify": true, 40 | "connection.blocked": true, 41 | "authentication_failure_close": true 42 | } 43 | }; 44 | 45 | // Construct the main frames used in the opening handshake 46 | function openFrames(vhost, query, credentials, extraClientProperties) { 47 | if (!vhost) 48 | vhost = '/'; 49 | else 50 | vhost = QS.unescape(vhost); 51 | 52 | var query = query || {}; 53 | 54 | function intOrDefault(val, def) { 55 | return (val === undefined) ? def : parseInt(val); 56 | } 57 | 58 | var clientProperties = Object.create(CLIENT_PROPERTIES); 59 | 60 | return { 61 | // start-ok 62 | 'clientProperties': copyInto(extraClientProperties, clientProperties), 63 | 'mechanism': credentials.mechanism, 64 | 'response': credentials.response(), 65 | 'locale': query.locale || 'en_US', 66 | 67 | // tune-ok 68 | 'channelMax': intOrDefault(query.channelMax, 0), 69 | 'frameMax': intOrDefault(query.frameMax, 131072), 70 | 'heartbeat': intOrDefault(query.heartbeat, 0), 71 | 72 | // open 73 | 'virtualHost': vhost, 74 | 'capabilities': '', 75 | 'insist': 0 76 | }; 77 | } 78 | 79 | // Decide on credentials based on what we're supplied. 80 | function credentialsFromUrl(parts) { 81 | var user = 'guest', passwd = 'guest'; 82 | if (parts.username != '' || parts.password != '') { 83 | user = (parts.username) ? unescape(parts.username) : ''; 84 | passwd = (parts.password) ? unescape(parts.password) : ''; 85 | } 86 | return credentials.plain(user, passwd); 87 | } 88 | 89 | function connect(url, socketOptions, openCallback) { 90 | // tls.connect uses `util._extend()` on the options given it, which 91 | // copies only properties mentioned in `Object.keys()`, when 92 | // processing the options. So I have to make copies too, rather 93 | // than using `Object.create()`. 94 | var sockopts = clone(socketOptions || {}); 95 | url = url || 'amqp://localhost'; 96 | 97 | var noDelay = !!sockopts.noDelay; 98 | var timeout = sockopts.timeout; 99 | var keepAlive = !!sockopts.keepAlive; 100 | // 0 is default for node 101 | var keepAliveDelay = sockopts.keepAliveDelay || 0; 102 | 103 | var extraClientProperties = sockopts.clientProperties || {}; 104 | 105 | var protocol, fields; 106 | if (typeof url === 'object') { 107 | protocol = (url.protocol || 'amqp') + ':'; 108 | sockopts.host = url.hostname; 109 | sockopts.servername = sockopts.servername || url.hostname; 110 | sockopts.port = url.port || ((protocol === 'amqp:') ? 5672 : 5671); 111 | 112 | var user, pass; 113 | // Only default if both are missing, to have the same behaviour as 114 | // the stringly URL. 115 | if (url.username == undefined && url.password == undefined) { 116 | user = 'guest'; pass = 'guest'; 117 | } else { 118 | user = url.username || ''; 119 | pass = url.password || ''; 120 | } 121 | 122 | var config = { 123 | locale: url.locale, 124 | channelMax: url.channelMax, 125 | frameMax: url.frameMax, 126 | heartbeat: url.heartbeat, 127 | }; 128 | 129 | fields = openFrames(url.vhost, config, sockopts.credentials || credentials.plain(user, pass), extraClientProperties); 130 | } else { 131 | var parts = URL(url, true); // yes, parse the query string 132 | protocol = parts.protocol; 133 | sockopts.host = parts.hostname; 134 | sockopts.servername = sockopts.servername || parts.hostname; 135 | sockopts.port = parseInt(parts.port) || ((protocol === 'amqp:') ? 5672 : 5671); 136 | var vhost = parts.pathname ? parts.pathname.substr(1) : null; 137 | fields = openFrames(vhost, parts.query, sockopts.credentials || credentialsFromUrl(parts), extraClientProperties); 138 | } 139 | 140 | var sockok = false; 141 | var sock; 142 | 143 | function onConnect() { 144 | sockok = true; 145 | sock.setNoDelay(noDelay); 146 | if (keepAlive) sock.setKeepAlive(keepAlive, keepAliveDelay); 147 | 148 | var c = new Connection(sock); 149 | c.open(fields, function(err, ok) { 150 | // disable timeout once the connection is open, we don't want 151 | // it fouling things 152 | if (timeout) sock.setTimeout(0); 153 | if (err === null) { 154 | openCallback(null, c); 155 | } else { 156 | // The connection isn't closed by the server on e.g. wrong password 157 | sock.end(); 158 | sock.destroy(); 159 | openCallback(err); 160 | } 161 | }); 162 | } 163 | 164 | if (protocol === 'amqp:') { 165 | sock = require('net').connect(sockopts, onConnect); 166 | } 167 | else if (protocol === 'amqps:') { 168 | sock = require('tls').connect(sockopts, onConnect); 169 | } 170 | else { 171 | throw new Error("Expected amqp: or amqps: as the protocol; got " + protocol); 172 | } 173 | 174 | if (timeout) { 175 | sock.setTimeout(timeout, function() { 176 | sock.end(); 177 | sock.destroy(); 178 | openCallback(new Error('connect ETIMEDOUT')); 179 | }); 180 | } 181 | 182 | sock.once('error', function(err) { 183 | if (!sockok) openCallback(err); 184 | }); 185 | 186 | } 187 | 188 | module.exports.connect = connect; 189 | module.exports.credentialsFromUrl = credentialsFromUrl; 190 | -------------------------------------------------------------------------------- /lib/credentials.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | // Different kind of credentials that can be supplied when opening a 6 | // connection, corresponding to SASL mechanisms There's only two 7 | // useful mechanisms that RabbitMQ implements: 8 | // * PLAIN (send username and password in the plain) 9 | // * EXTERNAL (assume the server will figure out who you are from 10 | // context, i.e., your SSL certificate) 11 | var codec = require('./codec') 12 | 13 | module.exports.plain = function(user, passwd) { 14 | return { 15 | mechanism: 'PLAIN', 16 | response: function() { 17 | return Buffer.from(['', user, passwd].join(String.fromCharCode(0))) 18 | }, 19 | username: user, 20 | password: passwd 21 | } 22 | } 23 | 24 | module.exports.amqplain = function(user, passwd) { 25 | return { 26 | mechanism: 'AMQPLAIN', 27 | response: function() { 28 | const buffer = Buffer.alloc(16384); 29 | const size = codec.encodeTable(buffer, { LOGIN: user, PASSWORD: passwd}, 0); 30 | return buffer.subarray(4, size); 31 | }, 32 | username: user, 33 | password: passwd 34 | } 35 | } 36 | 37 | module.exports.external = function() { 38 | return { 39 | mechanism: 'EXTERNAL', 40 | response: function() { return Buffer.from(''); } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | 3 | function trimStack(stack, num) { 4 | return stack && stack.split('\n').slice(num).join('\n'); 5 | } 6 | 7 | function IllegalOperationError(msg, stack) { 8 | var tmp = new Error(); 9 | this.message = msg; 10 | this.stack = this.toString() + '\n' + trimStack(tmp.stack, 2); 11 | this.stackAtStateChange = stack; 12 | } 13 | inherits(IllegalOperationError, Error); 14 | 15 | IllegalOperationError.prototype.name = 'IllegalOperationError'; 16 | 17 | function stackCapture(reason) { 18 | var e = new Error(); 19 | return 'Stack capture: ' + reason + '\n' + 20 | trimStack(e.stack, 2); 21 | } 22 | 23 | module.exports.IllegalOperationError = IllegalOperationError; 24 | module.exports.stackCapture = stackCapture; 25 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | // Stringifying various things 6 | 7 | 'use strict'; 8 | 9 | var defs = require('./defs'); 10 | var format = require('util').format; 11 | var HEARTBEAT = require('./frame').HEARTBEAT; 12 | 13 | module.exports.closeMessage = function(close) { 14 | var code = close.fields.replyCode; 15 | return format('%d (%s) with message "%s"', 16 | code, defs.constant_strs[code], 17 | close.fields.replyText); 18 | } 19 | 20 | module.exports.methodName = function(id) { 21 | return defs.info(id).name; 22 | }; 23 | 24 | module.exports.inspect = function(frame, showFields) { 25 | if (frame === HEARTBEAT) { 26 | return ''; 27 | } 28 | else if (!frame.id) { 29 | return format('', 30 | frame.channel, frame.size); 31 | } 32 | else { 33 | var info = defs.info(frame.id); 34 | return format('<%s channel:%d%s>', info.name, frame.channel, 35 | (showFields) 36 | ? ' ' + JSON.stringify(frame.fields, undefined, 2) 37 | : ''); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/frame.js: -------------------------------------------------------------------------------- 1 | // The river sweeps through 2 | // Silt and twigs, gravel and leaves 3 | // Driving the wheel on 4 | 5 | 'use strict'; 6 | 7 | const ints = require('buffer-more-ints') 8 | var defs = require('./defs'); 9 | var constants = defs.constants; 10 | var decode = defs.decode; 11 | 12 | module.exports.PROTOCOL_HEADER = "AMQP" + String.fromCharCode(0, 0, 9, 1); 13 | 14 | /* 15 | Frame format: 16 | 17 | 0 1 3 7 size+7 size+8 18 | +------+---------+-------------+ +------------+ +-----------+ 19 | | type | channel | size | | payload | | frame-end | 20 | +------+---------+-------------+ +------------+ +-----------+ 21 | octet short long size octets octet 22 | 23 | In general I want to know those first three things straight away, so I 24 | can discard frames early. 25 | 26 | */ 27 | 28 | // framing constants 29 | var FRAME_METHOD = constants.FRAME_METHOD, 30 | FRAME_HEARTBEAT = constants.FRAME_HEARTBEAT, 31 | FRAME_HEADER = constants.FRAME_HEADER, 32 | FRAME_BODY = constants.FRAME_BODY, 33 | FRAME_END = constants.FRAME_END; 34 | 35 | // expected byte sizes for frame parts 36 | const TYPE_BYTES = 1 37 | const CHANNEL_BYTES = 2 38 | const SIZE_BYTES = 4 39 | const FRAME_HEADER_BYTES = TYPE_BYTES + CHANNEL_BYTES + SIZE_BYTES 40 | const FRAME_END_BYTES = 1 41 | 42 | /** 43 | * @typedef {{ 44 | * type: number, 45 | * channel: number, 46 | * size: number, 47 | * payload: Buffer, 48 | * rest: Buffer 49 | * }} FrameStructure 50 | */ 51 | 52 | /** 53 | * This is a polyfill which will read a big int 64 bit as a number. 54 | * @arg { Buffer } buffer 55 | * @arg { number } offset 56 | * @returns { number } 57 | */ 58 | function readInt64BE(buffer, offset) { 59 | /** 60 | * We try to use native implementation if available here because 61 | * buffer-more-ints does not 62 | */ 63 | if (typeof Buffer.prototype.readBigInt64BE === 'function') { 64 | return Number(buffer.readBigInt64BE(offset)) 65 | } 66 | 67 | return ints.readInt64BE(buffer, offset) 68 | } 69 | 70 | // %%% TESTME possibly better to cons the first bit and write the 71 | // second directly, in the absence of IO lists 72 | /** 73 | * Make a frame header 74 | * @arg { number } channel 75 | * @arg { Buffer } payload 76 | */ 77 | module.exports.makeBodyFrame = function (channel, payload) { 78 | const frameSize = FRAME_HEADER_BYTES + payload.length + FRAME_END_BYTES 79 | 80 | const frame = Buffer.alloc(frameSize) 81 | 82 | let offset = 0 83 | 84 | offset = frame.writeUInt8(FRAME_BODY, offset) 85 | offset = frame.writeUInt16BE(channel, offset) 86 | offset = frame.writeInt32BE(payload.length, offset) 87 | 88 | payload.copy(frame, offset) 89 | offset += payload.length 90 | 91 | frame.writeUInt8(FRAME_END, offset) 92 | 93 | return frame 94 | }; 95 | 96 | /** 97 | * Parse an AMQP frame 98 | * @arg { Buffer } bin 99 | * @arg { number } max 100 | * @returns { FrameStructure | boolean } 101 | */ 102 | function parseFrame(bin) { 103 | if (bin.length < FRAME_HEADER_BYTES) { 104 | return false 105 | } 106 | 107 | const type = bin.readUInt8(0) 108 | const channel = bin.readUInt16BE(1) 109 | const size = bin.readUInt32BE(3) 110 | 111 | const totalSize = FRAME_HEADER_BYTES + size + FRAME_END_BYTES 112 | 113 | if (bin.length < totalSize) { 114 | return false 115 | } 116 | 117 | const frameEnd = bin.readUInt8(FRAME_HEADER_BYTES + size) 118 | 119 | if (frameEnd !== FRAME_END) { 120 | throw new Error('Invalid frame') 121 | } 122 | 123 | return { 124 | type, 125 | channel, 126 | size, 127 | payload: bin.subarray(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + size), 128 | rest: bin.subarray(totalSize) 129 | } 130 | } 131 | 132 | module.exports.parseFrame = parseFrame; 133 | 134 | var HEARTBEAT = {channel: 0}; 135 | 136 | /** 137 | * Decode AMQP frame into JS object 138 | * @param { FrameStructure } frame 139 | * @returns 140 | */ 141 | module.exports.decodeFrame = (frame) => { 142 | const payload = frame.payload 143 | const channel = frame.channel 144 | 145 | switch (frame.type) { 146 | case FRAME_METHOD: { 147 | const id = payload.readUInt32BE(0) 148 | const args = payload.subarray(4) 149 | const fields = decode(id, args) 150 | return { id, channel, fields } 151 | } 152 | case FRAME_HEADER: { 153 | const id = payload.readUInt16BE(0) 154 | // const weight = payload.readUInt16BE(2) 155 | const size = readInt64BE(payload, 4) 156 | const flagsAndfields = payload.subarray(12) 157 | const fields = decode(id, flagsAndfields) 158 | return { id, channel, size, fields } 159 | } 160 | case FRAME_BODY: 161 | return { channel, content: payload } 162 | case FRAME_HEARTBEAT: 163 | return HEARTBEAT 164 | default: 165 | throw new Error('Unknown frame type ' + frame.type) 166 | } 167 | } 168 | 169 | // encoded heartbeat 170 | module.exports.HEARTBEAT_BUF = Buffer.from([constants.FRAME_HEARTBEAT, 171 | 0, 0, 0, 0, // size = 0 172 | 0, 0, // channel = 0 173 | constants.FRAME_END]); 174 | 175 | module.exports.HEARTBEAT = HEARTBEAT; -------------------------------------------------------------------------------- /lib/heartbeat.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | // Heartbeats. In AMQP both clients and servers may expect a heartbeat 6 | // frame if there is no activity on the connection for a negotiated 7 | // period of time. If there's no activity for two such intervals, the 8 | // server or client is allowed to close the connection on the 9 | // presumption that the other party is dead. 10 | // 11 | // The client has two jobs here: the first is to send a heartbeat 12 | // frame if it's not sent any frames for a while, so that the server 13 | // doesn't think it's dead; the second is to check periodically that 14 | // it's seen activity from the server, and to advise if there doesn't 15 | // appear to have been any for over two intervals. 16 | // 17 | // Node.JS timers are a bit unreliable, in that they endeavour only to 18 | // fire at some indeterminate point *after* the given time (rather 19 | // gives the lie to 'realtime', dunnit). Because the scheduler is just 20 | // an event loop, it's quite easy to delay timers indefinitely by 21 | // reacting to some I/O with a lot of computation. 22 | // 23 | // To mitigate this I need a bit of creative interpretation: 24 | // 25 | // - I'll schedule a server activity check for every `interval`, and 26 | // check just how much time has passed. It will overshoot by at 27 | // least a small margin; modulo missing timer deadlines, it'll 28 | // notice between two and three intervals after activity actually 29 | // stops (otherwise, at some point after two intervals). 30 | // 31 | // - Every `interval / 2` I'll check that we've sent something since 32 | // the last check, and if not, send a heartbeat frame. If we're 33 | // really too busy to even run the check for two whole heartbeat 34 | // intervals, there must be a lot of I (but not O, at least not on 35 | // the connection), or computation, in which case perhaps it's best 36 | // the server cuts us off anyway. Why `interval / 2`? Because the 37 | // edge case is that the client sent a frame just after a 38 | // heartbeat, which would mean I only send one after almost two 39 | // intervals. (NB a heartbeat counts as a send, so it'll be checked 40 | // at least twice before sending another) 41 | // 42 | // This design is based largely on RabbitMQ's heartbeating: 43 | // https://github.com/rabbitmq/rabbitmq-common/blob/master/src/rabbit_heartbeat.erl 44 | 45 | // %% Yes, I could apply the same 'actually passage of time' thing to 46 | // %% send as well as to recv. 47 | 48 | 'use strict'; 49 | 50 | var EventEmitter = require('events'); 51 | 52 | // Exported so that we can mess with it in tests 53 | module.exports.UNITS_TO_MS = 1000; 54 | 55 | class Heart extends EventEmitter { 56 | constructor (interval, checkSend, checkRecv) { 57 | super(); 58 | 59 | this.interval = interval; 60 | 61 | var intervalMs = interval * module.exports.UNITS_TO_MS; 62 | // Function#bind is my new best friend 63 | var beat = this.emit.bind(this, 'beat'); 64 | var timeout = this.emit.bind(this, 'timeout'); 65 | 66 | this.sendTimer = setInterval( 67 | this.runHeartbeat.bind(this, checkSend, beat), intervalMs / 2); 68 | 69 | // A timeout occurs if I see nothing for *two consecutive* intervals 70 | var recvMissed = 0; 71 | function missedTwo () { 72 | if (!checkRecv()) 73 | return (++recvMissed < 2); 74 | else { recvMissed = 0; return true; } 75 | } 76 | this.recvTimer = setInterval( 77 | this.runHeartbeat.bind(this, missedTwo, timeout), intervalMs); 78 | } 79 | 80 | clear () { 81 | clearInterval(this.sendTimer); 82 | clearInterval(this.recvTimer); 83 | } 84 | 85 | runHeartbeat (check, fail) { 86 | // Have we seen activity? 87 | if (!check()) 88 | fail(); 89 | } 90 | } 91 | 92 | module.exports.Heart = Heart; 93 | -------------------------------------------------------------------------------- /lib/mux.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | 5 | 'use strict'; 6 | 7 | // A Mux is an object into which other readable streams may be piped; 8 | // it then writes 'packets' from the upstreams to the given 9 | // downstream. 10 | 11 | var assert = require('assert'); 12 | 13 | var schedule = (typeof setImmediate === 'function') ? 14 | setImmediate : process.nextTick; 15 | 16 | class Mux { 17 | constructor (downstream) { 18 | this.newStreams = []; 19 | this.oldStreams = []; 20 | this.blocked = false; 21 | this.scheduledRead = false; 22 | 23 | this.out = downstream; 24 | var self = this; 25 | downstream.on('drain', function () { 26 | self.blocked = false; 27 | self._readIncoming(); 28 | }); 29 | } 30 | 31 | // There are 2 states we can be in: 32 | // - waiting for outbound capacity, which will be signalled by a 33 | // - 'drain' event on the downstream; or, 34 | // - no packets to send, waiting for an inbound buffer to have 35 | // packets, which will be signalled by a 'readable' event 36 | // If we write all packets available whenever there is outbound 37 | // capacity, we will either run out of outbound capacity (`#write` 38 | // returns false), or run out of packets (all calls to an 39 | // `inbound.read()` have returned null). 40 | _readIncoming () { 41 | 42 | // We may be sent here speculatively, if an incoming stream has 43 | // become readable 44 | if (this.blocked) return; 45 | 46 | var accepting = true; 47 | var out = this.out; 48 | 49 | // Try to read a chunk from each stream in turn, until all streams 50 | // are empty, or we exhaust our ability to accept chunks. 51 | function roundrobin (streams) { 52 | var s; 53 | while (accepting && (s = streams.shift())) { 54 | var chunk = s.read(); 55 | if (chunk !== null) { 56 | accepting = out.write(chunk); 57 | streams.push(s); 58 | } 59 | } 60 | } 61 | 62 | roundrobin(this.newStreams); 63 | 64 | // Either we exhausted the new queues, or we ran out of capacity. If 65 | // we ran out of capacity, all the remaining new streams (i.e., 66 | // those with packets left) become old streams. This effectively 67 | // prioritises streams that keep their buffers close to empty over 68 | // those that are constantly near full. 69 | if (accepting) { // all new queues are exhausted, write as many as 70 | // we can from the old streams 71 | assert.equal(0, this.newStreams.length); 72 | roundrobin(this.oldStreams); 73 | } 74 | else { // ran out of room 75 | assert(this.newStreams.length > 0, "Expect some new streams to remain"); 76 | Array.prototype.push.apply(this.oldStreams, this.newStreams); 77 | this.newStreams = []; 78 | } 79 | // We may have exhausted all the old queues, or run out of room; 80 | // either way, all we need to do is record whether we have capacity 81 | // or not, so any speculative reads will know 82 | this.blocked = !accepting; 83 | } 84 | 85 | _scheduleRead () { 86 | var self = this; 87 | 88 | if (!self.scheduledRead) { 89 | schedule(function () { 90 | self.scheduledRead = false; 91 | self._readIncoming(); 92 | }); 93 | self.scheduledRead = true; 94 | } 95 | } 96 | 97 | pipeFrom (readable) { 98 | var self = this; 99 | 100 | function enqueue () { 101 | self.newStreams.push(readable); 102 | self._scheduleRead(); 103 | } 104 | 105 | function cleanup () { 106 | readable.removeListener('readable', enqueue); 107 | readable.removeListener('error', cleanup); 108 | readable.removeListener('end', cleanup); 109 | readable.removeListener('unpipeFrom', cleanupIfMe); 110 | } 111 | function cleanupIfMe (dest) { 112 | if (dest === self) cleanup(); 113 | } 114 | 115 | readable.on('unpipeFrom', cleanupIfMe); 116 | readable.on('end', cleanup); 117 | readable.on('error', cleanup); 118 | readable.on('readable', enqueue); 119 | } 120 | 121 | unpipeFrom (readable) { 122 | readable.emit('unpipeFrom', this); 123 | } 124 | } 125 | 126 | module.exports.Mux = Mux; 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amqplib", 3 | "homepage": "http://amqp-node.github.io/amqplib/", 4 | "main": "./channel_api.js", 5 | "version": "0.10.8", 6 | "description": "An AMQP 0-9-1 (e.g., RabbitMQ) library and client.", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/amqp-node/amqplib.git" 10 | }, 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "dependencies": { 15 | "buffer-more-ints": "~1.0.0", 16 | "url-parse": "~1.5.10" 17 | }, 18 | "devDependencies": { 19 | "claire": "0.4.1", 20 | "mocha": "^9.2.2", 21 | "nyc": "^15.1.0", 22 | "uglify-js": "2.8.x" 23 | }, 24 | "scripts": { 25 | "test": "make test" 26 | }, 27 | "keywords": [ 28 | "AMQP", 29 | "AMQP 0-9-1", 30 | "RabbitMQ" 31 | ], 32 | "author": "Michael Bridgen ", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /test/bitset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const claire = require('claire'); 4 | const {BitSet} = require('../lib/bitset'); 5 | 6 | const { 7 | forAll, 8 | data: arb, 9 | label, 10 | choice, 11 | transform 12 | } = claire; 13 | 14 | const PosInt = transform(Math.floor, arb.Positive); 15 | 16 | const EmptyBitSet = label('bitset', transform( 17 | size => { 18 | return new BitSet(size); 19 | }, 20 | choice(arb.Nothing, PosInt))); 21 | 22 | suite('BitSet', () => { 23 | 24 | test('get bit', forAll(EmptyBitSet, PosInt) 25 | .satisfy((b, bit) => { 26 | b.set(bit); 27 | return b.get(bit); 28 | }).asTest()); 29 | 30 | test('clear bit', forAll(EmptyBitSet, PosInt) 31 | .satisfy((b, bit) => { 32 | b.set(bit); 33 | b.clear(bit); 34 | return !b.get(bit); 35 | }).asTest()); 36 | 37 | test('next set of empty', forAll(EmptyBitSet) 38 | .satisfy(b => { 39 | return b.nextSetBit(0) === -1; 40 | }).asTest()); 41 | 42 | test('next set of one bit', forAll(EmptyBitSet, PosInt) 43 | .satisfy((b, bit) => { 44 | b.set(bit); 45 | return b.nextSetBit(0) === bit; 46 | }).asTest()); 47 | 48 | test('next set same bit', forAll(EmptyBitSet, PosInt) 49 | .satisfy((b, bit) => { 50 | b.set(bit); 51 | return b.nextSetBit(bit) === bit; 52 | }).asTest()); 53 | 54 | test('next set following bit', forAll(EmptyBitSet, PosInt) 55 | .satisfy((b, bit) => { 56 | b.set(bit); 57 | return b.nextSetBit(bit+1) === -1; 58 | }).asTest()); 59 | 60 | test('next clear of empty', forAll(EmptyBitSet, PosInt) 61 | .satisfy((b, bit) => { return b.nextClearBit(bit) === bit; }) 62 | .asTest()); 63 | 64 | test('next clear of one set', forAll(EmptyBitSet, PosInt) 65 | .satisfy((b, bit) => { 66 | b.set(bit); 67 | return b.nextClearBit(bit) === bit + 1; 68 | }).asTest()); 69 | }); 70 | -------------------------------------------------------------------------------- /test/callback_api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var crypto = require('crypto'); 5 | var api = require('../callback_api'); 6 | var util = require('./util'); 7 | var schedule = util.schedule; 8 | var randomString = util.randomString; 9 | var kCallback = util.kCallback; 10 | var domain = require('domain'); 11 | 12 | var URL = process.env.URL || 'amqp://localhost'; 13 | 14 | function connect(cb) { 15 | api.connect(URL, {}, cb); 16 | } 17 | 18 | // Construct a node-style callback from a `done` function 19 | function doneCallback(done) { 20 | return function(err, _) { 21 | if (err == null) done(); 22 | else done(err); 23 | }; 24 | } 25 | 26 | function ignore() {} 27 | 28 | function twice(done) { 29 | var first = function(err) { 30 | if (err == undefined) second = done; 31 | else second = ignore, done(err); 32 | }; 33 | var second = function(err) { 34 | if (err == undefined) first = done; 35 | else first = ignore, done(err); 36 | }; 37 | return {first: function(err) { first(err); }, 38 | second: function(err) { second(err); }}; 39 | } 40 | 41 | // Adapt 'done' to a callback that's expected to fail 42 | function failCallback(done) { 43 | return function(err, _) { 44 | if (err == null) done(new Error('Expected failure, got ' + val)); 45 | else done(); 46 | }; 47 | } 48 | 49 | function waitForMessages(ch, q, k) { 50 | ch.checkQueue(q, function(e, ok) { 51 | if (e != null) return k(e); 52 | else if (ok.messageCount > 0) return k(null, ok); 53 | else schedule(waitForMessages.bind(null, ch, q, k)); 54 | }); 55 | } 56 | 57 | 58 | suite('connect', function() { 59 | 60 | test('at all', function(done) { 61 | connect(doneCallback(done)); 62 | }); 63 | 64 | }); 65 | 66 | suite('updateSecret', function() { 67 | test('updateSecret', function(done) { 68 | connect(kCallback(function(c) { 69 | c.updateSecret(Buffer.from('new secret'), 'no reason', doneCallback(done)); 70 | })); 71 | }); 72 | }); 73 | 74 | function channel_test_fn(method) { 75 | return function(name, options, chfun) { 76 | if (arguments.length === 2) { 77 | chfun = options; 78 | options = {}; 79 | } 80 | test(name, function(done) { 81 | connect(kCallback(function(c) { 82 | c[method](options, kCallback(function(ch) { 83 | chfun(ch, done); 84 | }, done)); 85 | }, done)); 86 | }); 87 | }; 88 | } 89 | var channel_test = channel_test_fn('createChannel'); 90 | var confirm_channel_test = channel_test_fn('createConfirmChannel'); 91 | 92 | suite('channel open', function() { 93 | 94 | channel_test('at all', function(ch, done) { 95 | done(); 96 | }); 97 | 98 | channel_test('open and close', function(ch, done) { 99 | ch.close(doneCallback(done)); 100 | }); 101 | 102 | }); 103 | 104 | suite('assert, check, delete', function() { 105 | 106 | channel_test('assert, check, delete queue', function(ch, done) { 107 | ch.assertQueue('test.cb.queue', {}, kCallback(function(q) { 108 | ch.checkQueue('test.cb.queue', kCallback(function(ok) { 109 | ch.deleteQueue('test.cb.queue', {}, doneCallback(done)); 110 | }, done)); 111 | }, done)); 112 | }); 113 | 114 | channel_test('assert, check, delete exchange', function(ch, done) { 115 | ch.assertExchange( 116 | 'test.cb.exchange', 'topic', {}, kCallback(function(ex) { 117 | ch.checkExchange('test.cb.exchange', kCallback(function(ok) { 118 | ch.deleteExchange('test.cb.exchange', {}, doneCallback(done)); 119 | }, done)); 120 | }, done)); 121 | }); 122 | 123 | channel_test('fail on check non-queue', function(ch, done) { 124 | var both = twice(done); 125 | ch.on('error', failCallback(both.first)); 126 | ch.checkQueue('test.cb.nothere', failCallback(both.second)); 127 | }); 128 | 129 | channel_test('fail on check non-exchange', function(ch, done) { 130 | var both = twice(done); 131 | ch.on('error', failCallback(both.first)); 132 | ch.checkExchange('test.cb.nothere', failCallback(both.second)); 133 | }); 134 | 135 | }); 136 | 137 | suite('bindings', function() { 138 | 139 | channel_test('bind queue', function(ch, done) { 140 | ch.assertQueue('test.cb.bindq', {}, kCallback(function(q) { 141 | ch.assertExchange( 142 | 'test.cb.bindex', 'fanout', {}, kCallback(function(ex) { 143 | ch.bindQueue(q.queue, ex.exchange, '', {}, 144 | doneCallback(done)); 145 | }, done)); 146 | }, done)); 147 | }); 148 | 149 | channel_test('bind exchange', function(ch, done) { 150 | ch.assertExchange( 151 | 'test.cb.bindex1', 'fanout', {}, kCallback(function(ex1) { 152 | ch.assertExchange( 153 | 'test.cb.bindex2', 'fanout', {}, kCallback(function(ex2) { 154 | ch.bindExchange(ex1.exchange, 155 | ex2.exchange, '', {}, 156 | doneCallback(done)); 157 | }, done)); 158 | }, done)); 159 | }); 160 | 161 | }); 162 | 163 | suite('sending messages', function() { 164 | 165 | channel_test('send to queue and consume noAck', function(ch, done) { 166 | var msg = randomString(); 167 | ch.assertQueue('', {exclusive: true}, function(e, q) { 168 | if (e !== null) return done(e); 169 | ch.consume(q.queue, function(m) { 170 | if (m.content.toString() == msg) done(); 171 | else done(new Error("message content doesn't match:" + 172 | msg + " =/= " + m.content.toString())); 173 | }, {noAck: true, exclusive: true}); 174 | ch.sendToQueue(q.queue, Buffer.from(msg)); 175 | }); 176 | }); 177 | 178 | channel_test('send to queue and consume ack', function(ch, done) { 179 | var msg = randomString(); 180 | ch.assertQueue('', {exclusive: true}, function(e, q) { 181 | if (e !== null) return done(e); 182 | ch.consume(q.queue, function(m) { 183 | if (m.content.toString() == msg) { 184 | ch.ack(m); 185 | done(); 186 | } 187 | else done(new Error("message content doesn't match:" + 188 | msg + " =/= " + m.content.toString())); 189 | }, {noAck: false, exclusive: true}); 190 | ch.sendToQueue(q.queue, Buffer.from(msg)); 191 | }); 192 | }); 193 | 194 | channel_test('send to and get from queue', function(ch, done) { 195 | ch.assertQueue('', {exclusive: true}, function(e, q) { 196 | if (e != null) return done(e); 197 | var msg = randomString(); 198 | ch.sendToQueue(q.queue, Buffer.from(msg)); 199 | waitForMessages(ch, q.queue, function(e, _) { 200 | if (e != null) return done(e); 201 | ch.get(q.queue, {noAck: true}, function(e, m) { 202 | if (e != null) 203 | return done(e); 204 | else if (!m) 205 | return done(new Error('Empty (false) not expected')); 206 | else if (m.content.toString() == msg) 207 | return done(); 208 | else 209 | return done( 210 | new Error('Messages do not match: ' + 211 | msg + ' =/= ' + m.content.toString())); 212 | }); 213 | }); 214 | }); 215 | }); 216 | 217 | var channelOptions = {}; 218 | 219 | channel_test('find high watermark', function(ch, done) { 220 | var msg = randomString(); 221 | var baseline = 0; 222 | ch.assertQueue('', {exclusive: true}, function(e, q) { 223 | if (e !== null) return done(e); 224 | while (ch.sendToQueue(q.queue, Buffer.from(msg))) { 225 | baseline++; 226 | }; 227 | channelOptions.highWaterMark = baseline * 2; 228 | done(); 229 | }) 230 | }); 231 | 232 | channel_test('set high watermark', channelOptions, function(ch, done) { 233 | var msg = randomString(); 234 | ch.assertQueue('', {exclusive: true}, function(e, q) { 235 | if (e !== null) return done(e); 236 | var ok; 237 | for (var i = 0; i < channelOptions.highWaterMark; i++) { 238 | ok = ch.sendToQueue(q.queue, Buffer.from(msg)); 239 | assert.equal(ok, true); 240 | } 241 | done(); 242 | }); 243 | }); 244 | }); 245 | 246 | suite('ConfirmChannel', function() { 247 | 248 | confirm_channel_test('Receive confirmation', function(ch, done) { 249 | // An unroutable message, on the basis that you're not allowed a 250 | // queue with an empty name, and you can't make bindings to the 251 | // default exchange. Tricky eh? 252 | ch.publish('', '', Buffer.from('foo'), {}, done); 253 | }); 254 | 255 | confirm_channel_test('Wait for confirms', function(ch, done) { 256 | for (var i=0; i < 1000; i++) { 257 | ch.publish('', '', Buffer.from('foo'), {}); 258 | } 259 | ch.waitForConfirms(done); 260 | }); 261 | 262 | var channelOptions = {}; 263 | 264 | confirm_channel_test('find high watermark', function(ch, done) { 265 | var msg = randomString(); 266 | var baseline = 0; 267 | ch.assertQueue('', {exclusive: true}, function(e, q) { 268 | if (e !== null) return done(e); 269 | while (ch.sendToQueue(q.queue, Buffer.from(msg))) { 270 | baseline++; 271 | }; 272 | channelOptions.highWaterMark = baseline * 2; 273 | done(); 274 | }) 275 | }); 276 | 277 | confirm_channel_test('set high watermark', channelOptions, function(ch, done) { 278 | var msg = randomString(); 279 | ch.assertQueue('', {exclusive: true}, function(e, q) { 280 | if (e !== null) return done(e); 281 | var ok; 282 | for (var i = 0; i < channelOptions.highWaterMark; i++) { 283 | ok = ch.sendToQueue(q.queue, Buffer.from(msg)); 284 | assert.equal(ok, true); 285 | } 286 | done(); 287 | }); 288 | }); 289 | 290 | }); 291 | 292 | suite("Error handling", function() { 293 | 294 | /* 295 | I don't like having to do this, but there appears to be something 296 | broken about domains in Node.JS v0.8 and mocha. Apparently it has to 297 | do with how mocha and domains hook into error propogation: 298 | https://github.com/visionmedia/mocha/issues/513 (summary: domains in 299 | Node.JS v0.8 don't prevent uncaughtException from firing, and that's 300 | what mocha uses to detect .. an uncaught exception). 301 | 302 | Using domains with amqplib *does* work in practice in Node.JS v0.8: 303 | that is, it's possible to throw an exception in a callback and deal 304 | with it in the active domain, and thereby avoid it crashing the 305 | program. 306 | */ 307 | if (util.versionGreaterThan(process.versions.node, '0.8')) { 308 | test('Throw error in connection open callback', function(done) { 309 | var dom = domain.createDomain(); 310 | dom.on('error', failCallback(done)); 311 | connect(dom.bind(function(err, conn) { 312 | throw new Error('Spurious connection open callback error'); 313 | })); 314 | }); 315 | } 316 | 317 | // TODO: refactor {error_test, channel_test} 318 | function error_test(name, fun) { 319 | test(name, function(done) { 320 | var dom = domain.createDomain(); 321 | dom.run(function() { 322 | connect(kCallback(function(c) { 323 | // Seems like there were some unironed wrinkles in 0.8's 324 | // implementation of domains; explicitly adding the connection 325 | // to the domain makes sure any exception thrown in the course 326 | // of processing frames is handled by the domain. For other 327 | // versions of Node.JS, this ends up being belt-and-braces. 328 | dom.add(c); 329 | c.createChannel(kCallback(function(ch) { 330 | fun(ch, done, dom); 331 | }, done)); 332 | }, done)); 333 | }); 334 | }); 335 | } 336 | 337 | error_test('Channel open callback throws an error', function(ch, done, dom) { 338 | dom.on('error', failCallback(done)); 339 | throw new Error('Error in open callback'); 340 | }); 341 | 342 | error_test('RPC callback throws error', function(ch, done, dom) { 343 | dom.on('error', failCallback(done)); 344 | ch.prefetch(0, false, function(err, ok) { 345 | throw new Error('Spurious callback error'); 346 | }); 347 | }); 348 | 349 | error_test('Get callback throws error', function(ch, done, dom) { 350 | dom.on('error', failCallback(done)); 351 | ch.assertQueue('test.cb.get-with-error', {}, function(err, ok) { 352 | ch.get('test.cb.get-with-error', {noAck: true}, function() { 353 | throw new Error('Spurious callback error'); 354 | }); 355 | }); 356 | }); 357 | 358 | error_test('Consume callback throws error', function(ch, done, dom) { 359 | dom.on('error', failCallback(done)); 360 | ch.assertQueue('test.cb.consume-with-error', {}, function(err, ok) { 361 | ch.consume('test.cb.consume-with-error', ignore, {noAck: true}, function() { 362 | throw new Error('Spurious callback error'); 363 | }); 364 | }); 365 | }); 366 | 367 | error_test('Get from non-queue invokes error k', function(ch, done, dom) { 368 | var both = twice(failCallback(done)); 369 | dom.on('error', both.first); 370 | ch.get('', {}, both.second); 371 | }); 372 | 373 | error_test('Consume from non-queue invokes error k', function(ch, done, dom) { 374 | var both = twice(failCallback(done)); 375 | dom.on('error', both.first); 376 | ch.consume('', both.second); 377 | }); 378 | 379 | }); 380 | -------------------------------------------------------------------------------- /test/codec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var codec = require('../lib/codec'); 4 | var defs = require('../lib/defs'); 5 | var assert = require('assert'); 6 | var ints = require('buffer-more-ints'); 7 | var C = require('claire'); 8 | var forAll = C.forAll; 9 | 10 | // These just test known encodings; to generate the answers I used 11 | // RabbitMQ's binary generator module. 12 | 13 | var testCases = [ 14 | // integers 15 | ['byte', {byte: 112}, [4,98,121,116,101,98,112]], 16 | ['byte max value', {byte: 127}, [4,98,121,116,101,98,127]], 17 | ['byte min value', {byte: -128}, [4,98,121,116,101,98,128]], 18 | ['< -128 promoted to signed short', {short: -129}, [5,115,104,111,114,116,115,255,127]], 19 | ['> 127 promoted to short', {short: 128}, [5,115,104,111,114,116,115,0,128]], 20 | ['< 2^15 still a short', {short: 0x7fff}, [5,115,104,111,114,116,115,127,255]], 21 | ['-2^15 still a short', {short: -0x8000}, [5,115,104,111,114,116,115,128,0]], 22 | ['>= 2^15 promoted to int', {int: 0x8000}, [3,105,110,116,73,0,0,128,0]], 23 | ['< -2^15 promoted to int', {int: -0x8001}, [3,105,110,116,73,255,255,127,255]], 24 | ['< 2^31 still an int', {int: 0x7fffffff}, [3,105,110,116,73,127,255,255,255]], 25 | ['>= -2^31 still an int', {int: -0x80000000}, [3,105,110,116,73,128,0,0,0]], 26 | ['>= 2^31 promoted to long', {long: 0x80000000}, [4,108,111,110,103,108,0,0,0,0,128,0,0,0]], 27 | ['< -2^31 promoted to long', {long: -0x80000001}, [4,108,111,110,103,108,255,255,255,255,127,255,255,255]], 28 | 29 | // floating point 30 | ['float value', {double: 0.5}, [6,100,111,117,98,108,101,100,63,224,0,0,0,0,0,0]], 31 | ['negative float value', {double: -0.5}, [6,100,111,117,98,108,101,100,191,224,0,0,0,0,0,0]], 32 | // %% test some boundaries of precision? 33 | 34 | // string 35 | ['string', {string: "boop"}, [6,115,116,114,105,110,103,83,0,0,0,4,98,111,111,112]], 36 | 37 | // buffer -> byte array 38 | ['byte array from buffer', {bytes: Buffer.from([1,2,3,4])}, 39 | [5,98,121,116,101,115,120,0,0,0,4,1,2,3,4]], 40 | 41 | // boolean, void 42 | ['true', {bool: true}, [4,98,111,111,108,116,1]], 43 | ['false', {bool: false}, [4,98,111,111,108,116,0]], 44 | ['null', {'void': null}, [4,118,111,105,100,86]], 45 | 46 | // array, object 47 | ['array', {array: [6, true, "foo"]},[5,97,114,114,97,121,65,0,0,0,12,98,6,116,1,83,0,0,0,3,102,111,111]], 48 | ['object', {object: {foo: "bar", baz: 12}},[6,111,98,106,101,99,116,70,0,0,0,18,3,102,111,111,83,0,0,0,3,98,97,114,3,98,97,122,98,12]], 49 | 50 | // exotic types 51 | ['timestamp', {timestamp: {'!': 'timestamp', value: 1357212277527}},[9,116,105,109,101,115,116,97,109,112,84,0,0,1,60,0,39,219,23]], 52 | ['decimal', {decimal: {'!': 'decimal', value: {digits: 2345, places: 2}}},[7,100,101,99,105,109,97,108,68,2,0,0,9,41]], 53 | ['float', {float: {'!': 'float', value: 0.1}},[5,102,108,111,97,116,102,61,204,204,205]], 54 | ['unsignedbyte', {unsignedbyte:{'!': 'unsignedbyte', value: 255}}, [12,117,110,115,105,103,110,101,100,98,121,116,101,66,255]], 55 | ['unsignedshort', {unsignedshort:{'!': 'unsignedshort', value: 65535}}, [13,117,110,115,105,103,110,101,100,115,104,111,114,116,117,255,255]], 56 | ['unsignedint', {unsignedint:{'!': 'unsignedint', value: 4294967295}}, [11,117,110,115,105,103,110,101,100,105,110,116,105,255,255,255,255]], 57 | ]; 58 | 59 | function bufferToArray(b) { 60 | return Array.prototype.slice.call(b); 61 | } 62 | 63 | suite("Implicit encodings", function() { 64 | 65 | testCases.forEach(function(tc) { 66 | var name = tc[0], val = tc[1], expect = tc[2]; 67 | test(name, function() { 68 | var buffer = Buffer.alloc(1000); 69 | var size = codec.encodeTable(buffer, val, 0); 70 | var result = buffer.subarray(4, size); 71 | assert.deepEqual(expect, bufferToArray(result)); 72 | }); 73 | }); 74 | }); 75 | 76 | // Whole frames 77 | 78 | var amqp = require('./data'); 79 | 80 | function roundtrip_table(t) { 81 | var buf = Buffer.alloc(4096); 82 | var size = codec.encodeTable(buf, t, 0); 83 | var decoded = codec.decodeFields(buf.subarray(4, size)); // ignore the length-prefix 84 | try { 85 | assert.deepEqual(removeExplicitTypes(t), decoded); 86 | } 87 | catch (e) { return false; } 88 | return true; 89 | } 90 | 91 | function roundtrips(T) { 92 | return forAll(T).satisfy(function(v) { return roundtrip_table({value: v}); }); 93 | } 94 | 95 | suite("Roundtrip values", function() { 96 | [ 97 | amqp.Octet, 98 | amqp.ShortStr, 99 | amqp.LongStr, 100 | amqp.UShort, 101 | amqp.ULong, 102 | amqp.ULongLong, 103 | amqp.UShort, 104 | amqp.Short, 105 | amqp.Long, 106 | amqp.Bit, 107 | amqp.Decimal, 108 | amqp.Timestamp, 109 | amqp.UnsignedByte, 110 | amqp.UnsignedShort, 111 | amqp.UnsignedInt, 112 | amqp.Double, 113 | amqp.Float, 114 | amqp.FieldArray, 115 | amqp.FieldTable 116 | ].forEach(function(T) { 117 | test(T.toString() + ' roundtrip', roundtrips(T).asTest()); 118 | }); 119 | }); 120 | 121 | // When encoding, you can supply explicitly-typed fields like `{'!': 122 | // int32, 50}`. Most of these do not appear in the decoded values, so 123 | // to compare like-to-like we have to remove them from the input. 124 | function removeExplicitTypes(input) { 125 | switch (typeof input) { 126 | case 'object': 127 | if (input == null) { 128 | return null; 129 | } 130 | if (Array.isArray(input)) { 131 | var newArr = []; 132 | for (var i = 0; i < input.length; i++) { 133 | newArr[i] = removeExplicitTypes(input[i]); 134 | } 135 | return newArr; 136 | } 137 | if (Buffer.isBuffer(input)) { 138 | return input; 139 | } 140 | switch (input['!']) { 141 | case 'timestamp': 142 | case 'decimal': 143 | case 'float': 144 | return input; 145 | case undefined: 146 | var newObj = {} 147 | for (var k in input) { 148 | newObj[k] = removeExplicitTypes(input[k]); 149 | } 150 | return newObj; 151 | default: 152 | return input.value; 153 | } 154 | 155 | default: 156 | return input; 157 | } 158 | } 159 | 160 | // Asserts that the decoded fields are equal to the original fields, 161 | // or equal to a default where absent in the original. The defaults 162 | // depend on the type of method or properties. 163 | // 164 | // This works slightly different for methods and properties: for 165 | // methods, each field must have a value, so the default is 166 | // substituted for undefined values when encoding; for properties, 167 | // fields may be absent in the encoded value, so a default is 168 | // substituted for missing fields when decoding. The effect is the 169 | // same so far as these tests are concerned. 170 | function assertEqualModuloDefaults(original, decodedFields) { 171 | var args = defs.info(original.id).args; 172 | for (var i=0; i < args.length; i++) { 173 | var arg = args[i]; 174 | var originalValue = original.fields[arg.name]; 175 | var decodedValue = decodedFields[arg.name]; 176 | try { 177 | if (originalValue === undefined) { 178 | // longstr gets special treatment here, since the defaults are 179 | // given as strings rather than buffers, but the decoded values 180 | // will be buffers. 181 | assert.deepEqual((arg.type === 'longstr') ? 182 | Buffer.from(arg.default) : arg.default, 183 | decodedValue); 184 | } 185 | else { 186 | assert.deepEqual(removeExplicitTypes(originalValue), decodedValue); 187 | } 188 | } 189 | catch (assertionErr) { 190 | var methodOrProps = defs.info(original.id).name; 191 | assertionErr.message += ' (frame ' + methodOrProps + 192 | ' field ' + arg.name + ')'; 193 | throw assertionErr; 194 | } 195 | } 196 | // %%% TODO make sure there's no surplus fields 197 | return true; 198 | } 199 | 200 | // This is handy for elsewhere 201 | module.exports.assertEqualModuloDefaults = assertEqualModuloDefaults; 202 | 203 | function roundtripMethod(Method) { 204 | return forAll(Method).satisfy(function(method) { 205 | var buf = defs.encodeMethod(method.id, 0, method.fields); 206 | // FIXME depends on framing, ugh 207 | var fs1 = defs.decode(method.id, buf.subarray(11, buf.length)); 208 | assertEqualModuloDefaults(method, fs1); 209 | return true; 210 | }); 211 | } 212 | 213 | function roundtripProperties(Properties) { 214 | return forAll(Properties).satisfy(function(properties) { 215 | var buf = defs.encodeProperties(properties.id, 0, properties.size, 216 | properties.fields); 217 | // FIXME depends on framing, ugh 218 | var fs1 = defs.decode(properties.id, buf.subarray(19, buf.length)); 219 | assert.equal(properties.size, ints.readUInt64BE(buf, 11)); 220 | assertEqualModuloDefaults(properties, fs1); 221 | return true; 222 | }); 223 | } 224 | 225 | suite("Roundtrip methods", function() { 226 | amqp.methods.forEach(function(Method) { 227 | test(Method.toString() + ' roundtrip', 228 | roundtripMethod(Method).asTest()); 229 | }); 230 | }); 231 | 232 | suite("Roundtrip properties", function() { 233 | amqp.properties.forEach(function(Properties) { 234 | test(Properties.toString() + ' roundtrip', 235 | roundtripProperties(Properties).asTest()); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/connect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var connect = require('../lib/connect').connect; 4 | var credentialsFromUrl = require('../lib/connect').credentialsFromUrl; 5 | var defs = require('../lib/defs'); 6 | var assert = require('assert'); 7 | var util = require('./util'); 8 | var net = require('net'); 9 | var fail = util.fail, succeed = util.succeed, latch = util.latch, 10 | kCallback = util.kCallback, 11 | succeedIfAttributeEquals = util.succeedIfAttributeEquals; 12 | var format = require('util').format; 13 | 14 | var URL = process.env.URL || 'amqp://localhost'; 15 | 16 | var urlparse = require('url-parse'); 17 | 18 | suite("Credentials", function() { 19 | 20 | function checkCreds(creds, user, pass, done) { 21 | if (creds.mechanism != 'PLAIN') { 22 | return done('expected mechanism PLAIN'); 23 | } 24 | if (creds.username != user || creds.password != pass) { 25 | return done(format("expected '%s', '%s'; got '%s', '%s'", 26 | user, pass, creds.username, creds.password)); 27 | } 28 | done(); 29 | } 30 | 31 | test("no creds", function(done) { 32 | var parts = urlparse('amqp://localhost'); 33 | var creds = credentialsFromUrl(parts); 34 | checkCreds(creds, 'guest', 'guest', done); 35 | }); 36 | test("usual user:pass", function(done) { 37 | var parts = urlparse('amqp://user:pass@localhost') 38 | var creds = credentialsFromUrl(parts); 39 | checkCreds(creds, 'user', 'pass', done); 40 | }); 41 | test("missing user", function(done) { 42 | var parts = urlparse('amqps://:password@localhost'); 43 | var creds = credentialsFromUrl(parts); 44 | checkCreds(creds, '', 'password', done); 45 | }); 46 | test("missing password", function(done) { 47 | var parts = urlparse('amqps://username:@localhost'); 48 | var creds = credentialsFromUrl(parts); 49 | checkCreds(creds, 'username', '', done); 50 | }); 51 | test("escaped colons", function(done) { 52 | var parts = urlparse('amqp://user%3Aname:pass%3Aword@localhost') 53 | var creds = credentialsFromUrl(parts); 54 | checkCreds(creds, 'user:name', 'pass:word', done); 55 | }); 56 | }); 57 | 58 | suite("Connect API", function() { 59 | 60 | test("Connection refused", function(done) { 61 | connect('amqp://localhost:23450', {}, 62 | kCallback(fail(done), succeed(done))); 63 | }); 64 | 65 | // %% this ought to fail the promise, rather than throwing an error 66 | test("bad URL", function() { 67 | assert.throws(function() { 68 | connect('blurble'); 69 | }); 70 | }); 71 | 72 | test("wrongly typed open option", function(done) { 73 | var url = require('url'); 74 | var parts = url.parse(URL, true); 75 | var q = parts.query || {}; 76 | q.frameMax = 'NOT A NUMBER'; 77 | parts.query = q; 78 | var u = url.format(parts); 79 | connect(u, {}, kCallback(fail(done), succeed(done))); 80 | }); 81 | 82 | test("serverProperties", function(done) { 83 | var url = require('url'); 84 | var parts = url.parse(URL, true); 85 | var config = parts.query || {}; 86 | connect(config, {}, function(err, connection) { 87 | if (err) { return done(err); } 88 | assert.equal(connection.serverProperties.product, 'RabbitMQ'); 89 | done(); 90 | }); 91 | }); 92 | 93 | test("using custom heartbeat option", function(done) { 94 | var url = require('url'); 95 | var parts = url.parse(URL, true); 96 | var config = parts.query || {}; 97 | config.heartbeat = 20; 98 | connect(config, {}, kCallback(succeedIfAttributeEquals('heartbeat', 20, done), fail(done))); 99 | }); 100 | 101 | test("wrongly typed heartbeat option", function(done) { 102 | var url = require('url'); 103 | var parts = url.parse(URL, true); 104 | var config = parts.query || {}; 105 | config.heartbeat = 'NOT A NUMBER'; 106 | connect(config, {}, kCallback(fail(done), succeed(done))); 107 | }); 108 | 109 | test("using plain credentials", function(done) { 110 | var url = require('url'); 111 | var parts = url.parse(URL, true); 112 | var u = 'guest', p = 'guest'; 113 | if (parts.auth) { 114 | var auth = parts.auth.split(":"); 115 | u = auth[0], p = auth[1]; 116 | } 117 | connect(URL, {credentials: require('../lib/credentials').plain(u, p)}, 118 | kCallback(succeed(done), fail(done))); 119 | }); 120 | 121 | test("using amqplain credentials", function(done) { 122 | var url = require('url'); 123 | var parts = url.parse(URL, true); 124 | var u = 'guest', p = 'guest'; 125 | if (parts.auth) { 126 | var auth = parts.auth.split(":"); 127 | u = auth[0], p = auth[1]; 128 | } 129 | connect(URL, {credentials: require('../lib/credentials').amqplain(u, p)}, 130 | kCallback(succeed(done), fail(done))); 131 | }); 132 | 133 | test("using unsupported mechanism", function(done) { 134 | var creds = { 135 | mechanism: 'UNSUPPORTED', 136 | response: function() { return Buffer.from(''); } 137 | }; 138 | connect(URL, {credentials: creds}, 139 | kCallback(fail(done), succeed(done))); 140 | }); 141 | 142 | test("with a given connection timeout", function(done) { 143 | var timeoutServer = net.createServer(function() {}).listen(31991); 144 | 145 | connect('amqp://localhost:31991', {timeout: 50}, function(err, val) { 146 | timeoutServer.close(); 147 | if (val) done(new Error('Expected connection timeout, did not')); 148 | else done(); 149 | }); 150 | }); 151 | }); 152 | 153 | suite('Errors on connect', function() { 154 | var server 155 | teardown(function() { 156 | if (server) { 157 | server.close(); 158 | } 159 | }) 160 | 161 | test("closes underlying connection on authentication error", function(done) { 162 | var bothDone = latch(2, done); 163 | server = net.createServer(function(socket) { 164 | socket.once('data', function(protocolHeader) { 165 | assert.deepStrictEqual( 166 | protocolHeader, 167 | Buffer.from("AMQP" + String.fromCharCode(0,0,9,1)) 168 | ); 169 | util.runServer(socket, function(send, wait) { 170 | send(defs.ConnectionStart, 171 | {versionMajor: 0, 172 | versionMinor: 9, 173 | serverProperties: {}, 174 | mechanisms: Buffer.from('PLAIN'), 175 | locales: Buffer.from('en_US')}); 176 | wait(defs.ConnectionStartOk)().then(function() { 177 | send(defs.ConnectionClose, 178 | {replyCode: 403, 179 | replyText: 'ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN', 180 | classId: 0, 181 | methodId: 0}); 182 | }); 183 | }); 184 | }); 185 | 186 | // Wait for the connection to be closed after the authentication error 187 | socket.once('end', function() { 188 | bothDone(); 189 | }); 190 | }).listen(0); 191 | 192 | connect('amqp://localhost:' + server.address().port, {}, function(err) { 193 | if (!err) bothDone(new Error('Expected authentication error')); 194 | bothDone(); 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var defs = require('../lib/defs'); 5 | var Connection = require('../lib/connection').Connection; 6 | var HEARTBEAT = require('../lib/frame').HEARTBEAT; 7 | var HB_BUF = require('../lib/frame').HEARTBEAT_BUF; 8 | var util = require('./util'); 9 | var succeed = util.succeed, fail = util.fail, latch = util.latch; 10 | var completes = util.completes; 11 | var kCallback = util.kCallback; 12 | 13 | var LOG_ERRORS = process.env.LOG_ERRORS; 14 | 15 | var OPEN_OPTS = { 16 | // start-ok 17 | 'clientProperties': {}, 18 | 'mechanism': 'PLAIN', 19 | 'response': Buffer.from(['', 'guest', 'guest'].join(String.fromCharCode(0))), 20 | 'locale': 'en_US', 21 | 22 | // tune-ok 23 | 'channelMax': 0, 24 | 'frameMax': 0, 25 | 'heartbeat': 0, 26 | 27 | // open 28 | 'virtualHost': '/', 29 | 'capabilities': '', 30 | 'insist': 0 31 | }; 32 | module.exports.OPEN_OPTS = OPEN_OPTS; 33 | 34 | function happy_open(send, wait) { 35 | // kick it off 36 | send(defs.ConnectionStart, 37 | {versionMajor: 0, 38 | versionMinor: 9, 39 | serverProperties: {}, 40 | mechanisms: Buffer.from('PLAIN'), 41 | locales: Buffer.from('en_US')}); 42 | return wait(defs.ConnectionStartOk)() 43 | .then(function(f) { 44 | send(defs.ConnectionTune, 45 | {channelMax: 0, 46 | heartbeat: 0, 47 | frameMax: 0}); 48 | }) 49 | .then(wait(defs.ConnectionTuneOk)) 50 | .then(wait(defs.ConnectionOpen)) 51 | .then(function(f) { 52 | send(defs.ConnectionOpenOk, 53 | {knownHosts: ''}); 54 | }); 55 | } 56 | module.exports.connection_handshake = happy_open; 57 | 58 | function connectionTest(client, server) { 59 | return function(done) { 60 | var bothDone = latch(2, done); 61 | var pair = util.socketPair(); 62 | var c = new Connection(pair.client); 63 | if (LOG_ERRORS) c.on('error', console.warn); 64 | client(c, bothDone); 65 | 66 | // NB only not a race here because the writes are synchronous 67 | var protocolHeader = pair.server.read(8); 68 | assert.deepEqual(Buffer.from("AMQP" + String.fromCharCode(0,0,9,1)), 69 | protocolHeader); 70 | 71 | var s = util.runServer(pair.server, function(send, wait) { 72 | server(send, wait, bothDone, pair.server); 73 | }); 74 | }; 75 | } 76 | 77 | suite("Connection errors", function() { 78 | 79 | test("socket close during open", function(done) { 80 | // RabbitMQ itself will take at least 3 seconds to close the socket 81 | // in the event of a handshake problem. Instead of using a live 82 | // connection, I'm just going to pretend. 83 | var pair = util.socketPair(); 84 | var conn = new Connection(pair.client); 85 | pair.server.on('readable', function() { 86 | pair.server.end(); 87 | }); 88 | conn.open({}, kCallback(fail(done), succeed(done))); 89 | }); 90 | 91 | test("bad frame during open", function(done) { 92 | var ss = util.socketPair(); 93 | var conn = new (require('../lib/connection').Connection)(ss.client); 94 | ss.server.on('readable', function() { 95 | ss.server.write(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])); 96 | }); 97 | conn.open({}, kCallback(fail(done), succeed(done))); 98 | }); 99 | 100 | }); 101 | 102 | suite("Connection open", function() { 103 | 104 | test("happy", connectionTest( 105 | function(c, done) { 106 | c.open(OPEN_OPTS, kCallback(succeed(done), fail(done))); 107 | }, 108 | function(send, wait, done) { 109 | happy_open(send, wait).then(succeed(done), fail(done)); 110 | })); 111 | 112 | test("wrong first frame", connectionTest( 113 | function(c, done) { 114 | c.open(OPEN_OPTS, kCallback(fail(done), succeed(done))); 115 | }, 116 | function(send, wait, done) { 117 | // bad server! bad! whatever were you thinking? 118 | completes(function() { 119 | send(defs.ConnectionTune, 120 | {channelMax: 0, 121 | heartbeat: 0, 122 | frameMax: 0}); 123 | }, done); 124 | })); 125 | 126 | test("unexpected socket close", connectionTest( 127 | function(c, done) { 128 | c.open(OPEN_OPTS, kCallback(fail(done), succeed(done))); 129 | }, 130 | function(send, wait, done, socket) { 131 | send(defs.ConnectionStart, 132 | {versionMajor: 0, 133 | versionMinor: 9, 134 | serverProperties: {}, 135 | mechanisms: Buffer.from('PLAIN'), 136 | locales: Buffer.from('en_US')}); 137 | return wait(defs.ConnectionStartOk)() 138 | .then(function() { 139 | socket.end(); 140 | }) 141 | .then(succeed(done), fail(done)); 142 | })); 143 | 144 | }); 145 | 146 | suite("Connection running", function() { 147 | 148 | test("wrong frame on channel 0", connectionTest( 149 | function(c, done) { 150 | c.on('error', succeed(done)); 151 | c.open(OPEN_OPTS); 152 | }, 153 | function(send, wait, done) { 154 | happy_open(send, wait) 155 | .then(function() { 156 | // there's actually nothing that would plausibly be sent to a 157 | // just opened connection, so this is violating more than one 158 | // rule. Nonetheless. 159 | send(defs.ChannelOpenOk, {channelId: Buffer.from('')}, 0); 160 | }) 161 | .then(wait(defs.ConnectionClose)) 162 | .then(function(close) { 163 | send(defs.ConnectionCloseOk, {}, 0); 164 | }).then(succeed(done), fail(done)); 165 | })); 166 | 167 | test("unopened channel", connectionTest( 168 | function(c, done) { 169 | c.on('error', succeed(done)); 170 | c.open(OPEN_OPTS); 171 | }, 172 | function(send, wait, done) { 173 | happy_open(send, wait) 174 | .then(function() { 175 | // there's actually nothing that would plausibly be sent to a 176 | // just opened connection, so this is violating more than one 177 | // rule. Nonetheless. 178 | send(defs.ChannelOpenOk, {channelId: Buffer.from('')}, 3); 179 | }) 180 | .then(wait(defs.ConnectionClose)) 181 | .then(function(close) { 182 | send(defs.ConnectionCloseOk, {}, 0); 183 | }).then(succeed(done), fail(done)); 184 | })); 185 | 186 | test("unexpected socket close", connectionTest( 187 | function(c, done) { 188 | var errorAndClosed = latch(2, done); 189 | c.on('error', succeed(errorAndClosed)); 190 | c.on('close', succeed(errorAndClosed)); 191 | c.open(OPEN_OPTS, kCallback(function() { 192 | c.sendHeartbeat(); 193 | }, fail(errorAndClosed))); 194 | }, 195 | function(send, wait, done, socket) { 196 | happy_open(send, wait) 197 | .then(wait()) 198 | .then(function() { 199 | socket.end(); 200 | }).then(succeed(done)); 201 | })); 202 | 203 | test("connection.blocked", connectionTest( 204 | function(c, done) { 205 | c.on('blocked', succeed(done)); 206 | c.open(OPEN_OPTS); 207 | }, 208 | function(send, wait, done, socket) { 209 | happy_open(send, wait) 210 | .then(function() { 211 | send(defs.ConnectionBlocked, {reason: 'felt like it'}, 0); 212 | }) 213 | .then(succeed(done)); 214 | })); 215 | 216 | test("connection.unblocked", connectionTest( 217 | function(c, done) { 218 | c.on('unblocked', succeed(done)); 219 | c.open(OPEN_OPTS); 220 | }, 221 | function(send, wait, done, socket) { 222 | happy_open(send, wait) 223 | .then(function() { 224 | send(defs.ConnectionUnblocked, {}, 0); 225 | }) 226 | .then(succeed(done)); 227 | })); 228 | 229 | 230 | }); 231 | 232 | suite("Connection close", function() { 233 | 234 | test("happy", connectionTest( 235 | function(c, done0) { 236 | var done = latch(2, done0); 237 | c.on('close', done); 238 | c.open(OPEN_OPTS, kCallback(function(_ok) { 239 | c.close(kCallback(succeed(done), fail(done))); 240 | }, function() {})); 241 | }, 242 | function(send, wait, done) { 243 | happy_open(send, wait) 244 | .then(wait(defs.ConnectionClose)) 245 | .then(function(close) { 246 | send(defs.ConnectionCloseOk, {}); 247 | }) 248 | .then(succeed(done), fail(done)); 249 | })); 250 | 251 | test("interleaved close frames", connectionTest( 252 | function(c, done0) { 253 | var done = latch(2, done0); 254 | c.on('close', done); 255 | c.open(OPEN_OPTS, kCallback(function(_ok) { 256 | c.close(kCallback(succeed(done), fail(done))); 257 | }, done)); 258 | }, 259 | function(send, wait, done) { 260 | happy_open(send, wait) 261 | .then(wait(defs.ConnectionClose)) 262 | .then(function(f) { 263 | send(defs.ConnectionClose, { 264 | replyText: "Ha!", 265 | replyCode: defs.constants.REPLY_SUCCESS, 266 | methodId: 0, classId: 0 267 | }); 268 | }) 269 | .then(wait(defs.ConnectionCloseOk)) 270 | .then(function(f) { 271 | send(defs.ConnectionCloseOk, {}); 272 | }) 273 | .then(succeed(done), fail(done)); 274 | })); 275 | 276 | test("server error close", connectionTest( 277 | function(c, done0) { 278 | var done = latch(2, done0); 279 | c.on('close', succeed(done)); 280 | c.on('error', succeed(done)); 281 | c.open(OPEN_OPTS); 282 | }, 283 | function(send, wait, done) { 284 | happy_open(send, wait) 285 | .then(function(f) { 286 | send(defs.ConnectionClose, { 287 | replyText: "Begone", 288 | replyCode: defs.constants.INTERNAL_ERROR, 289 | methodId: 0, classId: 0 290 | }); 291 | }) 292 | .then(wait(defs.ConnectionCloseOk)) 293 | .then(succeed(done), fail(done)); 294 | })); 295 | 296 | test("operator-intiated close", connectionTest( 297 | function(c, done) { 298 | c.on('close', succeed(done)); 299 | c.on('error', fail(done)); 300 | c.open(OPEN_OPTS); 301 | }, 302 | function(send, wait, done) { 303 | happy_open(send, wait) 304 | .then(function(f) { 305 | send(defs.ConnectionClose, { 306 | replyText: "Begone", 307 | replyCode: defs.constants.CONNECTION_FORCED, 308 | methodId: 0, classId: 0 309 | }); 310 | }) 311 | .then(wait(defs.ConnectionCloseOk)) 312 | .then(succeed(done), fail(done)); 313 | })); 314 | 315 | 316 | test("double close", connectionTest( 317 | function(c, done) { 318 | c.open(OPEN_OPTS, kCallback(function() { 319 | c.close(); 320 | // NB no synchronisation, we do this straight away 321 | assert.throws(function() { 322 | c.close(); 323 | }); 324 | done(); 325 | }, done)); 326 | }, 327 | function(send, wait, done) { 328 | happy_open(send, wait) 329 | .then(wait(defs.ConnectionClose)) 330 | .then(function() { 331 | send(defs.ConnectionCloseOk, {}); 332 | }) 333 | .then(succeed(done), fail(done)); 334 | })); 335 | 336 | }); 337 | 338 | suite("heartbeats", function() { 339 | 340 | var heartbeat = require('../lib/heartbeat'); 341 | 342 | setup(function() { 343 | heartbeat.UNITS_TO_MS = 20; 344 | }); 345 | 346 | teardown(function() { 347 | heartbeat.UNITS_TO_MS = 1000; 348 | }); 349 | 350 | test("send heartbeat after open", connectionTest( 351 | function(c, done) { 352 | completes(function() { 353 | var opts = Object.create(OPEN_OPTS); 354 | opts.heartbeat = 1; 355 | // Don't leave the error waiting to happen for the next test, this 356 | // confuses mocha awfully 357 | c.on('error', function() {}); 358 | c.open(opts); 359 | }, done); 360 | }, 361 | function(send, wait, done, socket) { 362 | var timer; 363 | happy_open(send, wait) 364 | .then(function() { 365 | timer = setInterval(function() { 366 | socket.write(HB_BUF); 367 | }, heartbeat.UNITS_TO_MS); 368 | }) 369 | .then(wait()) 370 | .then(function(hb) { 371 | if (hb === HEARTBEAT) done(); 372 | else done("Next frame after silence not a heartbeat"); 373 | clearInterval(timer); 374 | }); 375 | })); 376 | 377 | test("detect lack of heartbeats", connectionTest( 378 | function(c, done) { 379 | var opts = Object.create(OPEN_OPTS); 380 | opts.heartbeat = 1; 381 | c.on('error', succeed(done)); 382 | c.open(opts); 383 | }, 384 | function(send, wait, done, socket) { 385 | happy_open(send, wait) 386 | .then(succeed(done), fail(done)); 387 | // conspicuously not sending anything ... 388 | })); 389 | 390 | }); 391 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | // Property-based testing representations of various things in AMQP 2 | 3 | 'use strict'; 4 | 5 | var C = require('claire'); 6 | var forAll = C.forAll; 7 | var arb = C.data; 8 | var transform = C.transform; 9 | var repeat = C.repeat; 10 | var label = C.label; 11 | var sequence = C.sequence; 12 | var asGenerator = C.asGenerator; 13 | var sized = C.sized; 14 | var recursive = C.recursive; 15 | var choice = C.choice; 16 | var Undefined = C.Undefined; 17 | 18 | // Stub these out so we can use outside tests 19 | // if (!suite) var suite = function() {} 20 | // if (!test) var test = function() {} 21 | 22 | // These aren't exported in claire/index. so I could have to reproduce 23 | // them I guess. 24 | function choose(a, b) { 25 | return Math.random() * (b - a) + a; 26 | } 27 | 28 | function chooseInt(a, b) { 29 | return Math.floor(choose(a, b)); 30 | } 31 | 32 | function rangeInt(name, a, b) { 33 | return label(name, 34 | asGenerator(function(_) { return chooseInt(a, b); })); 35 | } 36 | 37 | function toFloat32(i) { 38 | var b = Buffer.alloc(4); 39 | b.writeFloatBE(i, 0); 40 | return b.readFloatBE(0); 41 | } 42 | 43 | function floatChooser(maxExp) { 44 | return function() { 45 | var n = Number.NaN; 46 | while (isNaN(n)) { 47 | var mantissa = Math.random() * 2 - 1; 48 | var exponent = chooseInt(0, maxExp); 49 | n = Math.pow(mantissa, exponent); 50 | } 51 | return n; 52 | } 53 | } 54 | 55 | function explicitType(t, underlying) { 56 | return label(t, transform(function(n) { 57 | return {'!': t, value: n}; 58 | }, underlying)); 59 | } 60 | 61 | // FIXME null, byte array, others? 62 | 63 | var Octet = rangeInt('octet', 0, 255); 64 | var ShortStr = label('shortstr', 65 | transform(function(s) { 66 | return s.substr(0, 255); 67 | }, arb.Str)); 68 | 69 | var LongStr = label('longstr', 70 | transform( 71 | function(bytes) { return Buffer.from(bytes); }, 72 | repeat(Octet))); 73 | 74 | var UShort = rangeInt('short-uint', 0, 0xffff); 75 | var ULong = rangeInt('long-uint', 0, 0xffffffff); 76 | var ULongLong = rangeInt('longlong-uint', 0, 0xffffffffffffffff); 77 | var Short = rangeInt('short-int', -0x8000, 0x7fff); 78 | var Long = rangeInt('long-int', -0x80000000, 0x7fffffff); 79 | var LongLong = rangeInt('longlong-int', -0x8000000000000000, 80 | 0x7fffffffffffffff); 81 | var Bit = label('bit', arb.Bool); 82 | var Double = label('double', asGenerator(floatChooser(308))); 83 | var Float = label('float', transform(toFloat32, floatChooser(38))); 84 | var Timestamp = label('timestamp', transform( 85 | function(n) { 86 | return {'!': 'timestamp', value: n}; 87 | }, ULongLong)); 88 | var Decimal = label('decimal', transform( 89 | function(args) { 90 | return {'!': 'decimal', value: {places: args[1], digits: args[0]}}; 91 | }, sequence(arb.UInt, Octet))); 92 | var UnsignedByte = label('unsignedbyte', transform( 93 | function(n) { 94 | return {'!': 'unsignedbyte', value: n}; 95 | }, Octet)); 96 | var UnsignedShort = label('unsignedshort', transform( 97 | function(n) { 98 | return {'!': 'unsignedshort', value: n}; 99 | }, UShort)); 100 | var UnsignedInt = label('unsignedint', transform( 101 | function(n) { 102 | return {'!': 'unsignedint', value: n}; 103 | }, ULong)); 104 | 105 | // Signed 8 bit int 106 | var Byte = rangeInt('byte', -128, 127); 107 | 108 | // Explicitly typed values 109 | var ExByte = explicitType('byte', Byte); 110 | var ExInt8 = explicitType('int8', Byte); 111 | var ExShort = explicitType('short', Short); 112 | var ExInt16 = explicitType('int16', Short); 113 | var ExInt = explicitType('int', Long); 114 | var ExInt32 = explicitType('int32', Long); 115 | var ExLong = explicitType('long', LongLong); 116 | var ExInt64 = explicitType('int64', LongLong); 117 | 118 | var FieldArray = label('field-array', recursive(function() { 119 | return arb.Array( 120 | arb.Null, 121 | LongStr, ShortStr, 122 | Octet, UShort, ULong, ULongLong, 123 | Byte, Short, Long, LongLong, 124 | ExByte, ExInt8, ExShort, ExInt16, 125 | ExInt, ExInt32, ExLong, ExInt64, 126 | Bit, Float, Double, FieldTable, FieldArray) 127 | })); 128 | 129 | var FieldTable = label('table', recursive(function() { 130 | return sized(function() { return 5; }, 131 | arb.Object( 132 | arb.Null, 133 | LongStr, ShortStr, Octet, 134 | UShort, ULong, ULongLong, 135 | Byte, Short, Long, LongLong, 136 | ExByte, ExInt8, ExShort, ExInt16, 137 | ExInt, ExInt32, ExLong, ExInt64, 138 | Bit, Float, Double, FieldArray, FieldTable)) 139 | })); 140 | 141 | // Internal tests of our properties 142 | var domainProps = [ 143 | [Octet, function(n) { return n >= 0 && n < 256; }], 144 | [ShortStr, function(s) { return typeof s === 'string' && s.length < 256; }], 145 | [LongStr, function(s) { return Buffer.isBuffer(s); }], 146 | [UShort, function(n) { return n >= 0 && n <= 0xffff; }], 147 | [ULong, function(n) { return n >= 0 && n <= 0xffffffff; }], 148 | [ULongLong, function(n) { 149 | return n >= 0 && n <= 0xffffffffffffffff; }], 150 | [Short, function(n) { return n >= -0x8000 && n <= 0x8000; }], 151 | [Long, function(n) { return n >= -0x80000000 && n < 0x80000000; }], 152 | [LongLong, function(n) { return n >= -0x8000000000000000 && n < 0x8000000000000000; }], 153 | [Bit, function(b) { return typeof b === 'boolean'; }], 154 | [Double, function(f) { return !isNaN(f) && isFinite(f); }], 155 | [Float, function(f) { return !isNaN(f) && isFinite(f) && (Math.log(Math.abs(f)) * Math.LOG10E) < 309; }], 156 | [Decimal, function(d) { 157 | return d['!'] === 'decimal' && 158 | d.value['places'] <= 255 && 159 | d.value['digits'] <= 0xffffffff; 160 | }], 161 | [Timestamp, function(t) { return t['!'] === 'timestamp'; }], 162 | [FieldTable, function(t) { return typeof t === 'object'; }], 163 | [FieldArray, function(a) { return Array.isArray(a); }] 164 | ]; 165 | 166 | suite("Domains", function() { 167 | domainProps.forEach(function(p) { 168 | test(p[0] + ' domain', 169 | forAll(p[0]).satisfy(p[1]).asTest({times: 500})); 170 | }); 171 | }); 172 | 173 | // For methods and properties (as opposed to field table values) it's 174 | // easier just to accept and produce numbers for timestamps. 175 | var ArgTimestamp = label('timestamp', ULongLong); 176 | 177 | // These are the domains used in method arguments 178 | var ARG_TYPES = { 179 | 'octet': Octet, 180 | 'shortstr': ShortStr, 181 | 'longstr': LongStr, 182 | 'short': UShort, 183 | 'long': ULong, 184 | 'longlong': ULongLong, 185 | 'bit': Bit, 186 | 'table': FieldTable, 187 | 'timestamp': ArgTimestamp 188 | }; 189 | 190 | function argtype(thing) { 191 | if (thing.default === undefined) { 192 | return ARG_TYPES[thing.type]; 193 | } 194 | else { 195 | return choice(ARG_TYPES[thing.type], Undefined); 196 | } 197 | } 198 | 199 | function zipObject(vals, names) { 200 | var obj = {}; 201 | vals.forEach(function(v, i) { obj[names[i]] = v; }); 202 | return obj; 203 | } 204 | 205 | function name(arg) { return arg.name; } 206 | 207 | var defs = require('../lib/defs'); 208 | 209 | function method(info) { 210 | var domain = sequence.apply(null, info.args.map(argtype)); 211 | var names = info.args.map(name); 212 | return label(info.name, transform(function(fieldVals) { 213 | return {id: info.id, 214 | fields: zipObject(fieldVals, names)}; 215 | }, domain)); 216 | } 217 | 218 | function properties(info) { 219 | var types = info.args.map(argtype); 220 | types.unshift(ULongLong); // size 221 | var domain = sequence.apply(null, types); 222 | var names = info.args.map(name); 223 | return label(info.name, transform(function(fieldVals) { 224 | return {id: info.id, 225 | size: fieldVals[0], 226 | fields: zipObject(fieldVals.slice(1), names)}; 227 | }, domain)); 228 | } 229 | 230 | var methods = []; 231 | var propertieses = []; 232 | 233 | for (var k in defs) { 234 | if (k.substr(0, 10) === 'methodInfo') { 235 | methods.push(method(defs[k])); 236 | methods[defs[k].name] = method(defs[k]); 237 | } 238 | else if (k.substr(0, 14) === 'propertiesInfo') { 239 | propertieses.push(properties(defs[k])); 240 | propertieses[defs[k].name] = properties(defs[k]); 241 | } 242 | }; 243 | 244 | module.exports = { 245 | Octet: Octet, 246 | ShortStr: ShortStr, 247 | LongStr: LongStr, 248 | UShort: UShort, 249 | ULong: ULong, 250 | ULongLong: ULongLong, 251 | Short: Short, 252 | Long: Long, 253 | LongLong: LongLong, 254 | Bit: Bit, 255 | Double: Double, 256 | Float: Float, 257 | Timestamp: Timestamp, 258 | Decimal: Decimal, 259 | UnsignedByte: UnsignedByte, 260 | UnsignedShort: UnsignedShort, 261 | UnsignedInt: UnsignedInt, 262 | FieldArray: FieldArray, 263 | FieldTable: FieldTable, 264 | 265 | methods: methods, 266 | properties: propertieses 267 | }; 268 | 269 | module.exports.rangeInt = rangeInt; 270 | -------------------------------------------------------------------------------- /test/frame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var succeed = require('./util').succeed; 5 | var fail = require('./util').fail; 6 | var connection = require('../lib/connection'); 7 | var Frames = connection.Connection; 8 | var HEARTBEAT = require('../lib/frame').HEARTBEAT; 9 | var Stream = require('stream'); 10 | var PassThrough = Stream.PassThrough; 11 | 12 | var defs = require('../lib/defs'); 13 | 14 | // We'll need to supply a stream which we manipulate ourselves 15 | 16 | function inputs() { 17 | // don't coalesce buffers, since that could mess up properties 18 | // (e.g., encoded frame size) 19 | return new PassThrough({objectMode: true}); 20 | } 21 | 22 | var HB = Buffer.from([defs.constants.FRAME_HEARTBEAT, 23 | 0, 0, // channel 0 24 | 0, 0, 0, 0, // zero size 25 | defs.constants.FRAME_END]); 26 | 27 | suite("Explicit parsing", function() { 28 | 29 | test('Parse heartbeat', function() { 30 | var input = inputs(); 31 | var frames = new Frames(input); 32 | input.write(HB); 33 | assert(frames.recvFrame() === HEARTBEAT); 34 | assert(!frames.recvFrame()); 35 | }); 36 | 37 | test('Parse partitioned', function() { 38 | var input = inputs(); 39 | var frames = new Frames(input); 40 | input.write(HB.subarray(0, 3)); 41 | assert(!frames.recvFrame()); 42 | input.write(HB.subarray(3)); 43 | assert(frames.recvFrame() === HEARTBEAT); 44 | assert(!frames.recvFrame()); 45 | }); 46 | 47 | function testBogusFrame(name, bytes) { 48 | test(name, function(done) { 49 | var input = inputs(); 50 | var frames = new Frames(input); 51 | frames.frameMax = 5; //for the max frame test 52 | input.write(Buffer.from(bytes)); 53 | frames.step(function(err, frame) { 54 | if (err != null) done(); 55 | else done(new Error('Was a bogus frame!')); 56 | }); 57 | }); 58 | } 59 | 60 | testBogusFrame('Wrong sized frame', 61 | [defs.constants.FRAME_BODY, 62 | 0,0, 0,0,0,0, // zero length 63 | 65, // but a byte! 64 | defs.constants.FRAME_END]); 65 | 66 | testBogusFrame('Unknown method frame', 67 | [defs.constants.FRAME_METHOD, 68 | 0,0, 0,0,0,4, 69 | 0,0,0,0, // garbage ID 70 | defs.constants.FRAME_END]); 71 | 72 | }); 73 | 74 | // Now for a bit more fun. 75 | 76 | var amqp = require('./data'); 77 | var claire = require('claire'); 78 | var choice = claire.choice; 79 | var forAll = claire.forAll; 80 | var repeat = claire.repeat; 81 | var label = claire.label; 82 | var sequence = claire.sequence; 83 | var transform = claire.transform; 84 | var sized = claire.sized; 85 | 86 | var assertEqualModuloDefaults = 87 | require('./codec').assertEqualModuloDefaults; 88 | 89 | var Trace = label('frame trace', 90 | repeat(choice.apply(choice, amqp.methods))); 91 | 92 | suite("Parsing", function() { 93 | 94 | function testPartitioning(partition) { 95 | return forAll(Trace).satisfy(function(t) { 96 | var bufs = []; 97 | var input = inputs(); 98 | var frames = new Frames(input); 99 | var i = 0, ex; 100 | frames.accept = function(f) { 101 | // A minor hack to make sure we get the assertion exception; 102 | // otherwise, it's just a test that we reached the line 103 | // incrementing `i` for each frame. 104 | try { 105 | assertEqualModuloDefaults(t[i], f.fields); 106 | } 107 | catch (e) { 108 | ex = e; 109 | } 110 | i++; 111 | }; 112 | 113 | t.forEach(function(f) { 114 | f.channel = 0; 115 | bufs.push(defs.encodeMethod(f.id, 0, f.fields)); 116 | }); 117 | 118 | partition(bufs).forEach(function (chunk) { input.write(chunk); }); 119 | frames.acceptLoop(); 120 | if (ex) throw ex; 121 | return i === t.length; 122 | }).asTest({times: 20}) 123 | }; 124 | 125 | test("Parse trace of methods", 126 | testPartitioning(function(bufs) { return bufs; })); 127 | 128 | test("Parse concat'd methods", 129 | testPartitioning(function(bufs) { 130 | return [Buffer.concat(bufs)]; 131 | })); 132 | 133 | test("Parse partitioned methods", 134 | testPartitioning(function(bufs) { 135 | var full = Buffer.concat(bufs); 136 | var onethird = Math.floor(full.length / 3); 137 | var twothirds = 2 * onethird; 138 | return [ 139 | full.subarray(0, onethird), 140 | full.subarray(onethird, twothirds), 141 | full.subarray(twothirds) 142 | ]; 143 | })); 144 | }); 145 | 146 | var FRAME_MAX_MAX = 4096 * 4; 147 | var FRAME_MAX_MIN = 4096; 148 | 149 | var FrameMax = amqp.rangeInt('frame max', 150 | FRAME_MAX_MIN, 151 | FRAME_MAX_MAX); 152 | 153 | var Body = sized(function(_n) { 154 | return Math.floor(Math.random() * FRAME_MAX_MAX); 155 | }, repeat(amqp.Octet)); 156 | 157 | var Content = transform(function(args) { 158 | return { 159 | method: args[0].fields, 160 | header: args[1].fields, 161 | body: Buffer.from(args[2]) 162 | } 163 | }, sequence(amqp.methods['BasicDeliver'], 164 | amqp.properties['BasicProperties'], Body)); 165 | 166 | suite("Content framing", function() { 167 | test("Adhere to frame max", 168 | forAll(Content, FrameMax).satisfy(function(content, max) { 169 | var input = inputs(); 170 | var frames = new Frames(input); 171 | frames.frameMax = max; 172 | frames.sendMessage( 173 | 0, 174 | defs.BasicDeliver, content.method, 175 | defs.BasicProperties, content.header, 176 | content.body); 177 | var f, i = 0, largest = 0; 178 | while (f = input.read()) { 179 | i++; 180 | if (f.length > largest) largest = f.length; 181 | if (f.length > max) { 182 | return false; 183 | } 184 | } 185 | // The ratio of frames to 'contents' should always be >= 2 186 | // (one properties frame and at least one content frame); > 2 187 | // indicates fragmentation. The largest is always, of course <= frame max 188 | //console.log('Frames: %d; frames per message: %d; largest frame %d', i, i / t.length, largest); 189 | return true; 190 | }).asTest()); 191 | }); 192 | -------------------------------------------------------------------------------- /test/mux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Mux = require('../lib/mux').Mux; 5 | var PassThrough = require('stream').PassThrough; 6 | 7 | var latch = require('./util').latch; 8 | var schedule = require('./util').schedule; 9 | 10 | function stream() { 11 | return new PassThrough({objectMode: true}); 12 | } 13 | 14 | function readAllObjects(s, cb) { 15 | var objs = []; 16 | 17 | function read() { 18 | var v = s.read(); 19 | while (v !== null) { 20 | objs.push(v); 21 | v = s.read(); 22 | } 23 | } 24 | 25 | s.on('end', function() { cb(objs); }); 26 | s.on('readable', read); 27 | 28 | read(); 29 | } 30 | 31 | test("single input", function(done) { 32 | var input = stream(); 33 | var output = stream(); 34 | input.on('end', function() { output.end() }); 35 | 36 | var mux = new Mux(output); 37 | mux.pipeFrom(input); 38 | 39 | var data = [1,2,3,4,5,6,7,8,9]; 40 | // not 0, it's treated specially by PassThrough for some reason. By 41 | // 'specially' I mean it breaks the stream. See e.g., 42 | // https://github.com/isaacs/readable-stream/pull/55 43 | data.forEach(function (chunk) { input.write(chunk); }); 44 | 45 | readAllObjects(output, function(vals) { 46 | assert.deepEqual(data, vals); 47 | done(); 48 | }); 49 | 50 | input.end(); 51 | }); 52 | 53 | test("single input, resuming stream", function(done) { 54 | var input = stream(); 55 | var output = stream(); 56 | input.on('end', function() { output.end() }); 57 | 58 | var mux = new Mux(output); 59 | mux.pipeFrom(input); 60 | 61 | // Streams might be blocked and become readable again, simulate this 62 | // using a special read function and a marker 63 | var data = [1,2,3,4,'skip',6,7,8,9]; 64 | 65 | var oldRead = input.read; 66 | input.read = function(size) { 67 | var val = oldRead.call(input, size) 68 | 69 | if (val === 'skip') { 70 | input.emit('readable'); 71 | return null 72 | } 73 | 74 | return val; 75 | } 76 | 77 | data.forEach(function (chunk) { input.write(chunk); }); 78 | 79 | readAllObjects(output, function(vals) { 80 | assert.deepEqual([1,2,3,4,6,7,8,9], vals); 81 | done(); 82 | }); 83 | 84 | input.end(); 85 | }); 86 | 87 | test("two sequential inputs", function(done) { 88 | var input1 = stream(); 89 | var input2 = stream(); 90 | var output = stream(); 91 | var mux = new Mux(output); 92 | mux.pipeFrom(input1); 93 | mux.pipeFrom(input2); 94 | 95 | var data = [1,2,3,4,5,6,7,8,9]; 96 | data.forEach(function(v) { input1.write(v); }); 97 | 98 | input1.on('end', function() { 99 | data.forEach(function (v) { input2.write(v); }); 100 | input2.end(); 101 | }); 102 | input2.on('end', function() { output.end(); }); 103 | 104 | input1.end(); 105 | readAllObjects(output, function(vs) { 106 | assert.equal(2 * data.length, vs.length); 107 | done(); 108 | }); 109 | }); 110 | 111 | test("two interleaved inputs", function(done) { 112 | var input1 = stream(); 113 | var input2 = stream(); 114 | var output = stream(); 115 | var mux = new Mux(output); 116 | mux.pipeFrom(input1); 117 | mux.pipeFrom(input2); 118 | 119 | var endLatch = latch(2, function() { output.end(); }); 120 | input1.on('end', endLatch); 121 | input2.on('end', endLatch); 122 | 123 | var data = [1,2,3,4,5,6,7,8,9]; 124 | data.forEach(function(v) { input1.write(v); }); 125 | input1.end(); 126 | 127 | data.forEach(function(v) { input2.write(v); }); 128 | input2.end(); 129 | 130 | readAllObjects(output, function(vs) { 131 | assert.equal(2 * data.length, vs.length); 132 | done(); 133 | }); 134 | }); 135 | 136 | test("unpipe", function(done) { 137 | var input = stream(); 138 | var output = stream(); 139 | var mux = new Mux(output); 140 | 141 | var pipedData = [1,2,3,4,5]; 142 | var unpipedData = [6,7,8,9]; 143 | 144 | mux.pipeFrom(input); 145 | 146 | schedule(function() { 147 | pipedData.forEach(function (chunk) { input.write(chunk); }); 148 | 149 | schedule(function() { 150 | mux.unpipeFrom(input); 151 | 152 | schedule(function() { 153 | unpipedData.forEach(function(chunk) { input.write(chunk); }); 154 | input.end(); 155 | schedule(function() { 156 | // exhaust so that 'end' fires 157 | var v; while (v = input.read()); 158 | }); 159 | }); 160 | }); 161 | 162 | }); 163 | 164 | input.on('end', function() { 165 | output.end(); 166 | }); 167 | 168 | readAllObjects(output, function(vals) { 169 | try { 170 | assert.deepEqual(pipedData, vals); 171 | done(); 172 | } 173 | catch (e) { done(e); } 174 | }); 175 | }); 176 | 177 | test("roundrobin", function(done) { 178 | var input1 = stream(); 179 | var input2 = stream(); 180 | var output = stream(); 181 | var mux = new Mux(output); 182 | 183 | mux.pipeFrom(input1); 184 | mux.pipeFrom(input2); 185 | 186 | var endLatch = latch(2, function() { output.end(); }); 187 | input1.on('end', endLatch); 188 | input2.on('end', endLatch); 189 | 190 | var ones = [1,1,1,1,1]; 191 | ones.forEach(function(v) { input1.write(v); }); 192 | input1.end(); 193 | 194 | var twos = [2,2,2,2,2]; 195 | twos.forEach(function(v) { input2.write(v); }); 196 | input2.end(); 197 | 198 | readAllObjects(output, function(vs) { 199 | assert.deepEqual([1,2,1,2,1,2,1,2,1,2], vs); 200 | done(); 201 | }); 202 | 203 | }); 204 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | var Connection = require('../lib/connection').Connection; 5 | var PassThrough = require('stream').PassThrough; 6 | var defs = require('../lib/defs'); 7 | var assert = require('assert'); 8 | 9 | var schedule = (typeof setImmediate === 'function') ? 10 | setImmediate : process.nextTick; 11 | 12 | function randomString() { 13 | var hash = crypto.createHash('sha1'); 14 | hash.update(crypto.randomBytes(64)); 15 | return hash.digest('base64'); 16 | } 17 | 18 | 19 | // Set up a socket pair {client, server}, such that writes to the 20 | // client are readable from the server, and writes to the server are 21 | // readable at the client. 22 | // 23 | // +---+ +---+ 24 | // | C | | S | 25 | // --write->| l |----->| e |--read--> 26 | // | i | | r | 27 | // <--read--| e |<-----| v |<-write-- 28 | // | n | | e | 29 | // | t | | r | 30 | // +---+ +---+ 31 | // 32 | // I also need to make sure that end called on either socket affects 33 | // the other. 34 | 35 | function socketPair() { 36 | var server = new PassThrough(); 37 | var client = new PassThrough(); 38 | server.write = client.push.bind(client); 39 | client.write = server.push.bind(server); 40 | function end(chunk, encoding) { 41 | if (chunk) this.push(chunk, encoding); 42 | this.push(null); 43 | } 44 | server.end = end.bind(client); 45 | client.end = end.bind(server); 46 | 47 | return {client: client, server: server}; 48 | } 49 | 50 | function runServer(socket, run) { 51 | var frames = new Connection(socket); 52 | // We will be closing the socket without doing a closing handshake, 53 | // so cheat 54 | frames.expectSocketClose = true; 55 | // We also need to create some channel buffers, again a cheat 56 | frames.freshChannel(null); 57 | frames.freshChannel(null); 58 | frames.freshChannel(null); 59 | 60 | function send(id, fields, channel, content) { 61 | channel = channel || 0; 62 | if (content) { 63 | schedule(function() { 64 | frames.sendMessage(channel, id, fields, 65 | defs.BasicProperties, fields, 66 | content); 67 | }); 68 | } 69 | else { 70 | schedule(function() { 71 | frames.sendMethod(channel, id, fields); 72 | }); 73 | } 74 | } 75 | 76 | function wait(method) { 77 | return function() { 78 | return new Promise(function(resolve, reject) { 79 | if (method) { 80 | frames.step(function(e, f) { 81 | if (e !== null) return reject(e); 82 | if (f.id === method) 83 | resolve(f); 84 | else 85 | reject(new Error("Expected method: " + method + 86 | ", got " + f.id)); 87 | }); 88 | } 89 | else { 90 | frames.step(function(e, f) { 91 | if (e !== null) return reject(e); 92 | else resolve(f); 93 | }); 94 | } 95 | }); 96 | }; 97 | } 98 | run(send, wait); 99 | return frames; 100 | } 101 | 102 | // Produce a callback that will complete the test successfully 103 | function succeed(done) { 104 | return function() { done(); } 105 | } 106 | 107 | // Produce a callback that will complete the test successfully 108 | // only if the value is an object, it has the specified 109 | // attribute, and its value is equals to the expected value 110 | function succeedIfAttributeEquals(attribute, value, done) { 111 | return function(object) { 112 | if (object && !(object instanceof Error) && value === object[attribute]) { 113 | return done(); 114 | } 115 | 116 | done(new Error(attribute + " is not equal to " + value)); 117 | }; 118 | } 119 | 120 | // Produce a callback that will fail the test, given either an error 121 | // (to be used as a failure continuation) or any other value (to be 122 | // used as a success continuation when failure is expected) 123 | function fail(done) { 124 | return function(err) { 125 | if (err instanceof Error) done(err); 126 | else done(new Error("Expected to fail, instead got " + err.toString())); 127 | } 128 | } 129 | 130 | // Create a function that will call done once it's been called itself 131 | // `count` times. If it's called with an error value, it will 132 | // immediately call done with that error value. 133 | function latch(count, done) { 134 | var awaiting = count; 135 | var alive = true; 136 | return function(err) { 137 | if (err instanceof Error && alive) { 138 | alive = false; 139 | done(err); 140 | } 141 | else { 142 | awaiting--; 143 | if (awaiting === 0 && alive) { 144 | alive = false; 145 | done(); 146 | } 147 | } 148 | }; 149 | } 150 | 151 | // Call a thunk with a continuation that will be called with an error 152 | // if the thunk throws one, or nothing if it runs to completion. 153 | function completes(thunk, done) { 154 | try { 155 | thunk(); 156 | done(); 157 | } 158 | catch (e) { done(e); } 159 | } 160 | 161 | // Construct a Node.JS-style callback from a success continuation and 162 | // an error continuation 163 | function kCallback(k, ek) { 164 | return function(err, val) { 165 | if (err === null) k && k(val); 166 | else ek && ek(err); 167 | }; 168 | } 169 | 170 | // A noddy way to make tests depend on the node version. 171 | function versionGreaterThan(actual, spec) { 172 | 173 | function int(e) { return parseInt(e); } 174 | 175 | var version = actual.split('.').map(int); 176 | var desired = spec.split('.').map(int); 177 | for (var i=0; i < desired.length; i++) { 178 | var a = version[i], b = desired[i]; 179 | if (a != b) return a > b; 180 | } 181 | return false; 182 | } 183 | 184 | suite('versionGreaterThan', function() { 185 | 186 | test('full spec', function() { 187 | assert(versionGreaterThan('0.8.26', '0.6.12')); 188 | assert(versionGreaterThan('0.8.26', '0.8.21')); 189 | }); 190 | 191 | test('partial spec', function() { 192 | assert(versionGreaterThan('0.9.12', '0.8')); 193 | }); 194 | 195 | test('not greater', function() { 196 | assert(!versionGreaterThan('0.8.12', '0.8.26')); 197 | assert(!versionGreaterThan('0.6.2', '0.6.12')); 198 | assert(!versionGreaterThan('0.8.29', '0.8')); 199 | }); 200 | 201 | test 202 | 203 | }); 204 | 205 | module.exports = { 206 | socketPair: socketPair, 207 | runServer: runServer, 208 | succeed: succeed, 209 | succeedIfAttributeEquals: succeedIfAttributeEquals, 210 | fail: fail, 211 | latch: latch, 212 | completes: completes, 213 | kCallback: kCallback, 214 | schedule: schedule, 215 | randomString: randomString, 216 | versionGreaterThan: versionGreaterThan 217 | }; 218 | --------------------------------------------------------------------------------