├── .gitignore ├── .nycrc.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── index.ts ├── package.json ├── src ├── log.ts ├── messageEmitter.ts ├── service.ts ├── types.ts └── utils.ts ├── tests ├── config.ts ├── failed_message.test.ts ├── priority_messages.test.ts └── send_receive_messages.test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | package-lock.json 65 | .idea 66 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMap": true, 3 | "all": true, 4 | "extension": [".ts"], 5 | "exclude": ["**/*.d.ts", "tests", "coverage"], 6 | "reporter": ["text-summary", "html", "lcovonly"] 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | services: rabbitmq 3 | os: 4 | - linux 5 | addons: 6 | apt: 7 | packages: 8 | - rabbitmq-server 9 | node_js: 10 | - 'stable' 11 | - 'lts/*' 12 | 13 | script: 14 | - npm run tslint 15 | - npm test 16 | 17 | cache: 18 | directories: 19 | - node_modules 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 4.0.0 / 2021-12-03 2 | ================== 3 | 4 | * Adds specific protocol option 5 | 6 | 3.0.0 / 2021-01-13 7 | ================== 8 | 9 | * License changed to MIT 10 | * Dependencies updated 11 | 12 | 2.0.0 / 2019-11-07 13 | ================== 14 | 15 | * InitService and closeService are now init and close respectively 16 | * MessageConcurrency option renamed to concurrency 17 | 18 | 1.3.0 / 2019-10-12 19 | ================== 20 | 21 | * RegisterQueue now support an array of queues 22 | * Minor code improvements 23 | * Dependencies updated 24 | * Typescript set as a devDependency 25 | * Tests removed from build 26 | 27 | 1.2.0 / 2019-07-31 28 | ================== 29 | 30 | * Added connectionRetryDelay option 31 | * Max retry attempts error will be thrown before next retry 32 | * Fixed wrong warning log that was being shown on successfull connections 33 | 34 | 1.1.0 / 2019-07-29 35 | ================== 36 | 37 | * SendToQueue will now resolve after rabbitmq acked 38 | * Max Reconnection Retries option 39 | * Logs improved 40 | 41 | 1.0.0 / 2019-07-26 42 | ================== 43 | 44 | * Priority option added to sendMessage 45 | 46 | 0.1.3 / 2019-07-24 47 | ================== 48 | 49 | * Minor fix in CI tests 50 | 51 | 0.1.2 / 2019-07-24 52 | ================== 53 | 54 | * Dev dependencies updated 55 | * Removed package-lock from repository 56 | 57 | 0.1.1 / 2019-07-09 58 | ================== 59 | 60 | * Added retry option 61 | 62 | 0.1.0 / 2019-06-08 63 | ================== 64 | 65 | * First Release 66 | * OnQueue decorator 67 | * Main RabbitMQ service 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Foundernest 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple Queue Decorator 2 | ====================== 3 | _by @foundernest_ 4 | 5 | [![npm](https://img.shields.io/npm/v/simple-queue-decorator.svg)](https://www.npmjs.com/package/simple-queue-decorator) 6 | [![Build Status](https://img.shields.io/travis/foundernest/simple-queue-decorator/master.svg?label=build)](https://travis-ci.org/foundernest/simple-queue-decorator) 7 | 8 | Simple decorator-based wrapper for easy RabbitMQ queuing. 9 | 10 | ## Features 11 | 12 | * Built-in connection recovery to queue. 13 | * Decorator to register queue listeners. 14 | * Send/Receive messages through the same connection. 15 | * Ack and messages retries already set. 16 | * Automatic creation of queues. 17 | * JSON serialization/deserialization of messages. 18 | * Messages priority. 19 | * Concurrency control. 20 | * Full Typescript support. 21 | * Queue ACK for messages sent. 22 | 23 | ## How To 24 | Both JavaScript and Typescript can be used, however, this library takes advantage of Typescript decorators. `OnQueue` decorator is only available using Typescript. 25 | 26 | > A running instance of RabbitMQ is needed, a docker-compose file is provided to use along with this library in a dev env (`docker-compose up -d rabbitmq`). **Do not use** the given image in production 27 | 28 | Init the service: 29 | 30 | ```ts 31 | import * as SimpleQueueDecorator from 'simple-queue-decorator' 32 | 33 | await SimpleQueueDecorator.init({ 34 | url: "127.0.0.1", 35 | user: "guest", 36 | password: "guest" 37 | }) 38 | 39 | await SimpleQueueDecorator.close(); // Closes the service 40 | ``` 41 | 42 | 43 | Consume Messages (this can be done before `init`): 44 | ```ts 45 | import { OnQueue } from 'simple-queue-decorator' 46 | 47 | class MyConsumer { 48 | 49 | @OnQueue('my-queue') 50 | public static async onMessageReceived(msg: any) { 51 | console.log("Message Received", msg.foo) 52 | await doSomethingWithMyMsg(msg) // If this returns a rejected promise, message will be re-queued once 53 | } 54 | } 55 | ``` 56 | 57 | > It is recommended to use static methods with queue decorator 58 | 59 | Send Messages (Service must be initiated beforehand): 60 | 61 | ```ts 62 | import * as SimpleQueueDecorator from 'simple-queue-decorator' 63 | 64 | SimpleQueueDecorator.sendMessage('my-queue', {foo: "my message name"}) 65 | 66 | ``` 67 | 68 | Messages can also be listened without using the decorator: 69 | 70 | ```ts 71 | import * as SimpleQueueDecorator from 'simple-queue-decorator' 72 | 73 | 74 | SimpleQueueDecorator.registerQueue("my-queue", async (msg) => { 75 | console.log("Message Received", msg.foo) 76 | await doSomethingWithMyMsg(msg) 77 | }) 78 | ``` 79 | 80 | This is preferable if using JavaScript or using dynamic dependencies (e.g. injectables) in the queue callback. RegisterQueue also support an array of queues, the callback will be execute for any message in any of those queues. 81 | 82 | Send a message with priority: 83 | 84 | ```ts 85 | import * as SimpleQueueDecorator from 'simple-queue-decorator' 86 | 87 | SimpleQueueDecorator.sendMessage('my-queue', 88 | {foo: "my message name"}, 89 | { 90 | priority: SimpleQueueDecorator.MessagePriority.HIGH 91 | }); 92 | ``` 93 | 94 | Messages will be ordered and received according to priority. Priority can be `LOW`, `MEDIUM` or `HIGH`, a number ranging from 1 to 10 can also be used. Keep in mind the following: 95 | * Messages with no priority set are considered lower than any priority (including `LOW`) 96 | * When messages are instantly consumed, order may not be guaranteed. 97 | 98 | 99 | The following options can be passed to `init`: 100 | 101 | * **url**: The plain url (amqp protocol) of rabbitMQ. 102 | * **user**: RabbitMQ user. 103 | * **password**: RabbitMQ password. 104 | * **log**: If true, will log internal queue errors, defaults to true. 105 | * **concurrency**: The number of messages to be consumed at the same time, defaults to 1. 106 | * **retry**: If true, 1 retry per message will be made if the callback returns a rejected promise, defaults to true. 107 | * **maxConnectionAttempts**: Maximum number of recconnection attempts on `init`, if 0, it will attempt indefinitely. Defaults to 0. 108 | * **connectionRetryDelay**: Milliseconds to wait before connection attempts. Defaults to 5 109 | * **protocol**: Specify the protocol (i.e: amqp, amqps, https, etc) 110 | 111 | ### Development steps 112 | node and npm required, either docker or a running instance of rabbitmq required. 113 | 114 | 1. `npm install` 115 | 2. `npm run tsc` to compile 116 | 3. (optional) `docker-compose up -d rabbitmq` to launch rabbitmq 117 | 4. `npm test` to compile and execute tests (rabbitmq must be running) 118 | 119 | ### Important Notes 120 | 121 | This library makes several assumptions on how the messages are going to be consumed, as such, if your needs are different, we recommend directly using [amqplib](https://www.npmjs.com/package/amqplib). 122 | 123 | * A single retry will be done before completely dropping a message. 124 | * Only one listener is attached to each queue. 125 | * A single connection to be shared between all consumers. 126 | * Queues are created with persistence. 127 | * Messages are JSON formatted. 128 | * By default, messages are consumed 1 at a time. 129 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | rabbitmq: 2 | image: rabbitmq:3-management-alpine 3 | ports: 4 | - '5672:5672' 5 | - '15672:15672' # Rabbitmq admin 6 | volumes: 7 | - rabbitmq:/var/lib/rabbitmq 8 | restart: always 9 | environment: 10 | - RABBITMQ_DEFAULT_USER=guest 11 | - RABBITMQ_DEFAULT_PASS=guest 12 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import RabbitMQService from './src/service' 2 | import { InitOptions, SendMessageOptions } from './src/types' 3 | 4 | const rabbitService = new RabbitMQService() 5 | 6 | type DecoratorFunction = ( 7 | target: any, 8 | propertyKey: string, 9 | propertyDescriptor: PropertyDescriptor 10 | ) => void 11 | 12 | export async function init(options: InitOptions): Promise { 13 | rabbitService.setOptions(options) 14 | await rabbitService.connect() 15 | } 16 | 17 | export function sendMessage( 18 | queue: string, 19 | msg: any, 20 | options: SendMessageOptions = {} 21 | ): Promise { 22 | return rabbitService.sendMessage(queue, msg, options) 23 | } 24 | 25 | export function close(): Promise { 26 | return rabbitService.disconnect() 27 | } 28 | 29 | export function registerQueue( 30 | queueName: string | string[], 31 | cb: (r: any) => Promise 32 | ): void { 33 | rabbitService.registerQueue(queueName, cb) 34 | } 35 | 36 | export function OnQueue(queueName: string): DecoratorFunction { 37 | return ( 38 | target: any, 39 | _propertyKey: string, 40 | propertyDescriptor: PropertyDescriptor 41 | ): void => { 42 | rabbitService.registerQueue( 43 | queueName, 44 | propertyDescriptor.value.bind(target) 45 | ) 46 | } 47 | } 48 | 49 | export enum MessagePriority { 50 | LOW = 2, 51 | MEDIUM = 4, 52 | HIGH = 7, 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-queue-decorator", 3 | "version": "4.0.0", 4 | "description": "A simple interface with RabbitMQ through typescript decorators", 5 | "main": "dist/index.js", 6 | "dependencies": { 7 | "amqplib": "^0.6.0" 8 | }, 9 | "devDependencies": { 10 | "@types/amqplib": "^0.5.17", 11 | "@types/mocha": "^8.2.0", 12 | "@types/node": "^14.14.20", 13 | "mocha": "^8.2.1", 14 | "nyc": "^15.1.0", 15 | "prettier": "^2.2.1", 16 | "ts-node": "^9.1.1", 17 | "tslint": "^5.20.1", 18 | "tslint-config-prettier": "^1.18.0", 19 | "typescript": "^4.1.3" 20 | }, 21 | "scripts": { 22 | "test": "nyc mocha --require ts-node/register ./tests/*.test.ts", 23 | "pretest": "npm run tsc --- --sourceMap", 24 | "tsc": "rm -rf dist && tsc", 25 | "tslint": "tslint 'src/**/*.ts?(x)'", 26 | "prepublishOnly": "npm run tslint && npm test && npm run tsc" 27 | }, 28 | "files": [ 29 | "dist/src", 30 | "dist/index.*" 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/foundernest/simple-queue-decorator.git" 35 | }, 36 | "keywords": [ 37 | "rabbitmq", 38 | "queue", 39 | "amqp", 40 | "typescript", 41 | "decorator" 42 | ], 43 | "author": "FounderNest", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/foundernest/simple-queue-decorator/issues" 47 | }, 48 | "prettier": { 49 | "tabWidth": 2, 50 | "semi": false, 51 | "singleQuote": true, 52 | "trailingComma": "es5" 53 | }, 54 | "homepage": "https://github.com/foundernest/simple-queue-decorator#readme" 55 | } 56 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | export default class Log { 2 | private active: boolean 3 | constructor(active: boolean = true) { 4 | this.active = active 5 | } 6 | 7 | public log(...args: any[]): void { 8 | if (this.active) { 9 | console.log(...args) 10 | } 11 | } 12 | 13 | public warn(...args: any[]): void { 14 | if (this.active) { 15 | console.warn(...args) 16 | } 17 | } 18 | 19 | public error(...args: any[]): void { 20 | if (this.active) { 21 | console.error(...args) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/messageEmitter.ts: -------------------------------------------------------------------------------- 1 | import { SendMessageOptions } from './types' 2 | import { ConfirmChannel } from 'amqplib' 3 | 4 | export default class MessageEmitter { 5 | private channel: ConfirmChannel 6 | 7 | constructor(channel: ConfirmChannel) { 8 | this.channel = channel 9 | } 10 | 11 | public sendMessage( 12 | queue: string, 13 | msg: any, 14 | options: SendMessageOptions 15 | ): Promise { 16 | return new Promise((resolve, reject) => { 17 | this.channel.sendToQueue( 18 | queue, 19 | Buffer.from(JSON.stringify(msg)), 20 | { 21 | persistent: true, 22 | priority: options.priority, 23 | }, 24 | (err) => { 25 | if (err) { 26 | reject(err) 27 | } else { 28 | resolve() 29 | } 30 | } 31 | ) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import amqp from 'amqplib' 2 | import { wait } from './utils' 3 | import { 4 | InitOptions, 5 | SendMessageOptions, 6 | ServiceOptions, 7 | DefaultOptions, 8 | } from './types' 9 | import Log from './log' 10 | import MessageEmitter from './messageEmitter' 11 | 12 | type QueueRegistry = { 13 | [q: string]: { 14 | cb: (r: any) => Promise 15 | connected: boolean // This is to make "consumeQueue" idempotent 16 | } 17 | } 18 | 19 | const DEFAULT_OPTIONS: DefaultOptions = { 20 | concurrency: 1, 21 | log: true, 22 | retry: true, 23 | connectionRetryDelay: 5000, 24 | } 25 | 26 | enum ServiceStatus { 27 | Connected, 28 | Idle, 29 | Connecting, 30 | } 31 | 32 | export default class RabbitMQService { 33 | private _options?: InitOptions 34 | private _channel?: amqp.ConfirmChannel 35 | private queueRegistry: QueueRegistry = {} 36 | private connection?: amqp.Connection 37 | private log: Log 38 | private assertedQueues: Set = new Set() // Avoid duplicated queues 39 | private status: ServiceStatus = ServiceStatus.Idle 40 | 41 | constructor(options?: InitOptions) { 42 | this.log = new Log(true) 43 | if (options) { 44 | this.setOptions(options) 45 | } 46 | } 47 | 48 | public setOptions(options: InitOptions): void { 49 | this._options = options 50 | this.log = new Log(options.log) 51 | } 52 | 53 | public async sendMessage( 54 | queue: string, 55 | msg: any, 56 | options: SendMessageOptions 57 | ): Promise { 58 | await this.assertQueue(queue) 59 | const emitter = new MessageEmitter(this.channel) 60 | await emitter.sendMessage(queue, msg, options) 61 | } 62 | 63 | public registerQueue( 64 | queueName: string | string[], 65 | cb: (r: any) => Promise 66 | ): void { 67 | if (Array.isArray(queueName)) { 68 | queueName.forEach((val) => this.registerSingleQueue(val, cb)) 69 | } else { 70 | this.registerSingleQueue(queueName, cb) 71 | } 72 | } 73 | 74 | public async connect(): Promise { 75 | this.disconnect() 76 | this.status = ServiceStatus.Connecting 77 | let retries = 0 78 | 79 | while (this.shouldRetryConnection(retries)) { 80 | retries++ 81 | try { 82 | this.log.log('[RabbitMQ] Connecting') 83 | this.connection = await amqp.connect(this.url) 84 | this._channel = await this.connection.createConfirmChannel() 85 | await this.channel.prefetch(this.options.concurrency) // Number of messages to fetch simultaneously 86 | this.connection.on('close', () => { 87 | this.connection = undefined 88 | this._channel = undefined 89 | // Unexpected close 90 | if (this.status === ServiceStatus.Connected) { 91 | this.log.warn('[RabbitMQ] Unexpected Close') 92 | this.connect() 93 | } 94 | }) 95 | await Promise.all( 96 | Object.keys(this.queueRegistry).map((queue) => { 97 | return this.consumeQueue(queue) 98 | }) 99 | ) 100 | 101 | this.status = ServiceStatus.Connected 102 | this.log.log('[RabbitMQ] Connected') 103 | } catch (err) { 104 | this.log.warn('[RabbitMQ] Error Connecting', err.message) 105 | if (!this.shouldRetryConnection(retries)) { 106 | throw new Error( 107 | `[RabbitMQ] Max Reconnection attemps [${retries}] - will not try to connect anymore.` 108 | ) 109 | } 110 | await wait(this.connectionDelay) 111 | } 112 | } 113 | } 114 | 115 | public async disconnect(): Promise { 116 | this.status = ServiceStatus.Idle 117 | for (const q of Object.keys(this.queueRegistry)) { 118 | this.queueRegistry[q].connected = false 119 | } 120 | if (this.connection) { 121 | await this.connection.close() 122 | } 123 | this.connection = undefined 124 | this._channel = undefined 125 | this.assertedQueues.clear() 126 | } 127 | 128 | private get options(): ServiceOptions { 129 | if (!this._options) { 130 | throw new Error('[RabbitMQService] Options not initialized') 131 | } 132 | return Object.assign({}, DEFAULT_OPTIONS, this._options) 133 | } 134 | 135 | private get url(): string { 136 | const protocol = !this.options.protocol ? 'amqp' : this.options.protocol 137 | return `${protocol}://${this.options.user}:${this.options.password}@${this.options.url}` 138 | } 139 | 140 | private get channel(): amqp.ConfirmChannel { 141 | if (!this._channel) { 142 | throw new Error('[RabbitMQ] Not Connected') 143 | } else { 144 | return this._channel 145 | } 146 | } 147 | 148 | private get connectionDelay(): number { 149 | const defaultConnectionDelay = 5000 150 | const delay = this.options.connectionRetryDelay 151 | return delay === undefined ? defaultConnectionDelay : delay 152 | } 153 | 154 | private shouldRetryConnection(retries: number): boolean { 155 | if (this.options.maxConnectionAttempts) { 156 | if (retries >= this.options.maxConnectionAttempts) { 157 | return false 158 | } 159 | } 160 | return this.status === ServiceStatus.Connecting 161 | } 162 | 163 | private async consumeQueue(queueName: string): Promise { 164 | const queueData = this.queueRegistry[queueName] 165 | if (this._channel && !queueData.connected) { 166 | queueData.connected = true 167 | await this.assertQueue(queueName) 168 | await this.channel.consume( 169 | queueName, 170 | async (msg: any) => { 171 | if (!msg) { 172 | this.log.error('[RabbitMQ] Message received is null') 173 | } else { 174 | try { 175 | const messageBody = JSON.parse(msg.content.toString()) 176 | await this.queueRegistry[queueName].cb(messageBody) 177 | if (this.channel) { 178 | this.channel.ack(msg) // acks that the message was processed 179 | } 180 | } catch (err) { 181 | // Error processing, it will be requeued unless it has already been delivered (1 retry) 182 | this.log.warn('Error Processing Message:', msg.content.toString()) 183 | if (err) { 184 | this.log.warn(err.message) 185 | } 186 | this.retryErroredMessageIfNeeded(msg) 187 | } 188 | } 189 | }, 190 | { noAck: false } 191 | ) 192 | } 193 | } 194 | 195 | private retryErroredMessageIfNeeded(msg: any): void { 196 | if (this.channel) { 197 | const shouldRetry = this.options.retry && !msg.fields.redelivered 198 | if (shouldRetry) { 199 | this.channel.nack(msg, false, true) 200 | } else { 201 | this.channel.nack(msg, false, false) 202 | } 203 | } 204 | } 205 | 206 | private async assertQueue(queueName: string): Promise { 207 | const queueAlreadyExists = this.assertedQueues.has(queueName) 208 | if (!queueAlreadyExists) { 209 | await this.channel.assertQueue(queueName, { 210 | durable: true, 211 | maxPriority: 10, 212 | }) // Creates the queue if doesn't exists 213 | this.assertedQueues.add(queueName) 214 | } 215 | } 216 | 217 | private registerSingleQueue( 218 | queueName: string, 219 | cb: (r: any) => Promise 220 | ): void { 221 | if (this.queueRegistry[queueName]) { 222 | throw new Error(`[RabbitMQService] queue ${queueName} already registered`) 223 | } 224 | this.queueRegistry[queueName] = { 225 | cb, 226 | connected: false, 227 | } 228 | this.consumeQueue(queueName) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type InitOptions = { 2 | url: string 3 | user: string 4 | password: string 5 | concurrency?: number 6 | log?: boolean 7 | retry?: boolean 8 | maxConnectionAttempts?: number 9 | connectionRetryDelay?: number 10 | protocol?: string 11 | } 12 | 13 | export type SendMessageOptions = { 14 | priority?: number 15 | } 16 | 17 | export type DefaultOptions = { 18 | concurrency: number 19 | log: boolean 20 | retry: boolean 21 | connectionRetryDelay: number 22 | } 23 | 24 | export type ServiceOptions = InitOptions & DefaultOptions 25 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function wait(ms: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve() 5 | }, ms) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /tests/config.ts: -------------------------------------------------------------------------------- 1 | const Config = { 2 | url: '127.0.0.1', 3 | user: 'guest', 4 | password: 'guest', 5 | log: false, 6 | } 7 | export default Config 8 | -------------------------------------------------------------------------------- /tests/failed_message.test.ts: -------------------------------------------------------------------------------- 1 | import { sendMessage, OnQueue, init, close } from '..' 2 | 3 | import Config from './config' 4 | import { EventEmitter } from 'events' 5 | import assert = require('assert') 6 | 7 | class BadWolpertingerTest { 8 | public static events: EventEmitter = new EventEmitter() 9 | 10 | @OnQueue('wolpertinger-bad-test-queue') 11 | public static async onMessageTest(msg: any): Promise { 12 | this.events.emit('msg', msg) 13 | return Promise.reject() 14 | } 15 | 16 | public static waitForNextMessage(): Promise { 17 | return new Promise((resolve) => { 18 | this.events.once('msg', (msg) => { 19 | resolve(msg) 20 | }) 21 | }) 22 | } 23 | } 24 | 25 | describe('Failed Messages', () => { 26 | afterEach(async () => { 27 | await close() 28 | }) 29 | 30 | it('Retry Failed Message Once', async () => { 31 | await init(Config) 32 | 33 | sendMessage('wolpertinger-bad-test-queue', { test: 'Bad1' }) 34 | const msg1 = await BadWolpertingerTest.waitForNextMessage() 35 | assert.strictEqual(msg1.test, 'Bad1') 36 | const msg2 = await BadWolpertingerTest.waitForNextMessage() 37 | assert.strictEqual(msg2.test, 'Bad1') 38 | 39 | return new Promise(async (resolve, reject) => { 40 | setTimeout(() => { 41 | resolve() 42 | }, 500) 43 | BadWolpertingerTest.waitForNextMessage().then(() => { 44 | reject(new Error('No more messages Expected')) 45 | }) 46 | }) 47 | }) 48 | 49 | it('Disable Retry Message', async () => { 50 | await init({ ...Config, retry: false }) 51 | sendMessage('wolpertinger-bad-test-queue', { test: 'Bad1' }) 52 | const msg1 = await BadWolpertingerTest.waitForNextMessage() 53 | assert.strictEqual(msg1.test, 'Bad1') 54 | 55 | return new Promise(async (resolve, reject) => { 56 | setTimeout(() => { 57 | resolve() 58 | }, 500) 59 | BadWolpertingerTest.waitForNextMessage().then(() => { 60 | reject(new Error('No more messages Expected')) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /tests/priority_messages.test.ts: -------------------------------------------------------------------------------- 1 | import { sendMessage, init, close, registerQueue, MessagePriority } from '..' 2 | 3 | import Config from './config' 4 | import assert = require('assert') 5 | 6 | describe('Send And Received Queue Messages', () => { 7 | before(async () => { 8 | await init(Config) 9 | }) 10 | 11 | after(async () => { 12 | await close() 13 | }) 14 | 15 | it('Priority Queue', (done) => { 16 | sendMessage( 17 | 'priority-test-queue', 18 | { content: 'msg1' }, 19 | { 20 | priority: MessagePriority.LOW, 21 | } 22 | ) 23 | sendMessage( 24 | 'priority-test-queue', 25 | { content: 'msg2' }, 26 | { 27 | priority: MessagePriority.LOW, 28 | } 29 | ) 30 | sendMessage( 31 | 'priority-test-queue', 32 | { content: 'msg3' }, 33 | { 34 | priority: MessagePriority.HIGH, 35 | } 36 | ) 37 | 38 | const receivedMessages: Array<{ content: string }> = [] 39 | registerQueue('priority-test-queue', (msg) => { 40 | receivedMessages.push(msg) 41 | if (receivedMessages.length === 3) { 42 | assert.strictEqual(receivedMessages[0].content, 'msg3') 43 | assert.strictEqual(receivedMessages[1].content, 'msg1') 44 | assert.strictEqual(receivedMessages[2].content, 'msg2') 45 | done() 46 | } 47 | return Promise.resolve() 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/send_receive_messages.test.ts: -------------------------------------------------------------------------------- 1 | import { sendMessage, OnQueue, init, close, registerQueue } from '..' 2 | 3 | import Config from './config' 4 | import { EventEmitter } from 'events' 5 | import assert = require('assert') 6 | 7 | class WolpertingerTest { 8 | public static events: EventEmitter = new EventEmitter() 9 | 10 | @OnQueue('wolpertinger-test-queue') 11 | public static async onMessageTest(msg: any): Promise { 12 | this.events.emit('msg', msg) 13 | } 14 | 15 | public static waitForNextMessage(): Promise { 16 | return new Promise((resolve) => { 17 | this.events.once('msg', (msg) => { 18 | resolve(msg) 19 | }) 20 | }) 21 | } 22 | } 23 | 24 | describe('Send And Received Queue Messages', () => { 25 | before(async () => { 26 | await init(Config) 27 | }) 28 | 29 | after(async () => { 30 | await close() 31 | }) 32 | 33 | it('Send Message', async () => { 34 | sendMessage('wolpertinger-test-queue', { test: 'Hola' }) 35 | const msg = await WolpertingerTest.waitForNextMessage() 36 | assert.strictEqual(msg.test, 'Hola') 37 | }) 38 | 39 | it('Queue Multiple Messages Before Reading Them', (done) => { 40 | for (let i = 0; i < 10; i++) { 41 | sendMessage('wolpertinger-test-queue-2', { item: i }) 42 | } 43 | 44 | class WolpertingerTest2 { 45 | @OnQueue('wolpertinger-test-queue-2') 46 | public static async onMessageTest(_msg: any): Promise { 47 | this.count++ 48 | if (this.count >= 10) { 49 | done() 50 | } 51 | } 52 | private static count = 0 53 | } 54 | }) 55 | 56 | it('Register Queue Without Decorator', async () => { 57 | const p = new Promise((resolve) => { 58 | function msgCallback(msg: any): Promise { 59 | assert.strictEqual(msg.test, 'arthur') 60 | resolve(undefined) 61 | return Promise.resolve() 62 | } 63 | registerQueue('no-decorator-queue', msgCallback) // No need for await, so registering is immediate 64 | }) 65 | await sendMessage('no-decorator-queue', { test: 'arthur' }) 66 | await p 67 | }) 68 | 69 | it('Register Queue Without Decorator (several queues)', async () => { 70 | let callbackCallCount = 0 71 | const p = new Promise((resolve) => { 72 | function msgCallback(msg: any): Promise { 73 | assert.strictEqual(msg.test, 'arthur') 74 | callbackCallCount++ 75 | if (callbackCallCount === 2) { 76 | resolve(undefined) 77 | } 78 | return Promise.resolve() 79 | } 80 | 81 | registerQueue( 82 | ['first-no-decorator-queue', 'second-no-decorator-queue'], 83 | msgCallback 84 | ) 85 | }) 86 | await sendMessage('first-no-decorator-queue', { test: 'arthur' }) 87 | await sendMessage('second-no-decorator-queue', { test: 'arthur' }) 88 | await p 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": false, 6 | "outDir": "dist", 7 | "sourceMap": false, 8 | "strict": true, 9 | "target": "es2015", 10 | "typeRoots": ["./@types", "./node_modules/@types"], 11 | "lib": ["es2017", "dom"], 12 | "declaration": true, 13 | "noUnusedParameters": false, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "experimentalDecorators": true 17 | }, 18 | "compileOnSave": true, 19 | "include": ["./src/**/*.ts", "index.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "no-unnecessary-initializer": false, 6 | "no-var-requires": true, 7 | "no-null-keyword": true, 8 | "no-consecutive-blank-lines": false, 9 | "quotemark": [true, "single", "avoid-escape"], 10 | "interface-name": false, 11 | "no-empty-interface": false, 12 | "no-namespace": false, 13 | "ordered-imports": false, 14 | "object-literal-sort-keys": false, 15 | "arrow-parens": false, 16 | "semicolon": [true, "never"], 17 | "member-ordering": [ 18 | true, 19 | { 20 | "order": [ 21 | "public-static-field", 22 | "public-static-method", 23 | "protected-static-field", 24 | "protected-static-method", 25 | "private-static-field", 26 | "private-static-method", 27 | "public-instance-field", 28 | "protected-instance-field", 29 | "private-instance-field", 30 | "public-constructor", 31 | "protected-constructor", 32 | "private-constructor", 33 | "public-instance-method", 34 | "protected-instance-method", 35 | "private-instance-method" 36 | ] 37 | } 38 | ], 39 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 40 | "no-inferrable-types": [true, "ignore-params"], 41 | "no-switch-case-fall-through": true, 42 | "typedef": [true, "call-signature", "parameter"], 43 | "trailing-comma": [false], 44 | "class-name": true, 45 | "max-classes-per-file": false, 46 | "curly": true, 47 | "eofline": true, 48 | "jsdoc-format": true, 49 | "member-access": true, 50 | "no-arg": true, 51 | "no-construct": true, 52 | "no-duplicate-variable": true, 53 | "no-empty": true, 54 | "no-eval": true, 55 | "no-internal-module": true, 56 | "no-string-literal": true, 57 | "no-trailing-whitespace": false, 58 | "no-unused-expression": true, 59 | "no-var-keyword": true, 60 | "one-line": [ 61 | true, 62 | "check-open-brace", 63 | "check-catch", 64 | "check-else", 65 | "check-finally", 66 | "check-whitespace" 67 | ], 68 | "switch-default": true, 69 | "triple-equals": [true, "allow-null-check"], 70 | "typedef-whitespace": [ 71 | true, 72 | { 73 | "call-signature": "nospace", 74 | "index-signature": "nospace", 75 | "parameter": "nospace", 76 | "property-declaration": "nospace", 77 | "variable-declaration": "nospace" 78 | } 79 | ], 80 | "variable-name": false, 81 | "whitespace": [ 82 | true, 83 | "check-branch", 84 | "check-decl", 85 | "check-operator", 86 | "check-type" 87 | ], 88 | "interface-over-type-literal": false, 89 | "no-debugger": false 90 | } 91 | } 92 | --------------------------------------------------------------------------------