├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .nycrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── abstractTest.js ├── bench.js ├── eslint.config.js ├── example.js ├── mqemitter.js ├── package.json ├── test ├── test.js └── types │ ├── index.ts │ └── tsconfig.json └── types ├── mqemitter.d.ts └── mqemitter.test-d.ts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: standard 10 | versions: 11 | - 16.0.3 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [20.x, 22.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Install 22 | run: | 23 | npm install 24 | 25 | - name: Run tests 26 | run: | 27 | npm run test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like nyc 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | .vscode 32 | *code-workspace 33 | 34 | # 0x 35 | flamegraph.html 36 | .__* 37 | profile* 38 | 39 | # mac files 40 | .DS_Store 41 | 42 | # vim swap files 43 | *.swp 44 | 45 | # lock files 46 | package-lock.json 47 | 48 | # generated code 49 | test/types/*.js 50 | test/types/*.map 51 | 52 | # test tap report 53 | out.tap 54 | 55 | libleveldb.so 56 | libleveldb.a 57 | test-data/ 58 | _benchdb_* 59 | 60 | package-lock.json -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": 100, 3 | "lines": 100, 4 | "functions": 100, 5 | "statements": 100, 6 | "check-coverage": true, 7 | "exclude": [ 8 | "abstractTest.js", 9 | "test/*.js" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@matteocollina.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2014-2020, Matteo Collina 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # MQEmitter 4 | 5 | ![ci](https://github.com/mcollina/mqemitter/workflows/ci/badge.svg) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/mcollina/mqemitter/badge.svg)](https://snyk.io/test/github/mcollina/mqemitter) 7 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)\ 8 | [![NPM version](https://img.shields.io/npm/v/mqemitter.svg?style=flat)](https://www.npmjs.com/mqemitter) 9 | [![NPM downloads](https://img.shields.io/npm/dm/mqemitter.svg?style=flat)](https://www.npmjs.com/mqemitter) 10 | 11 | An Opinionated Message Queue with an emitter-style API, but with callbacks. 12 | 13 | If you need a multi process MQEmitter, check out the table below: 14 | 15 | - [mqemitter-redis]: Redis-powered mqemitter 16 | - [mqemitter-mongodb]: Mongodb based mqemitter 17 | - [mqemitter-child-process]: Share the same mqemitter between a hierarchy of child processes 18 | - [mqemitter-cs]: Expose a MQEmitter via a simple client/server protocol 19 | - [mqemitter-p2p]: A P2P implementation of MQEmitter, based on HyperEmitter and a Merkle DAG 20 | - [mqemitter-aerospike]: Aerospike mqemitter 21 | 22 | ## Installation 23 | 24 | ```sh 25 | npm install mqemitter 26 | ``` 27 | 28 | ## Examples 29 | 30 | ```js 31 | const mq = require('mqemitter') 32 | const emitter = mq({ concurrency: 5 }) 33 | const message 34 | 35 | emitter.on('hello world', function (message, cb) { 36 | // call callback when you are done 37 | // do not pass any errors, the emitter cannot handle it. 38 | cb() 39 | }) 40 | 41 | // topic is mandatory 42 | message = { topic: 'hello world', payload: 'or any other fields' } 43 | emitter.emit(message, function () { 44 | // emitter will never return an error 45 | }) 46 | ``` 47 | 48 | ## API 49 | 50 | - [new MQEmitter ([options])](#new-mqemitter-options) 51 | - [emitter.emit (message, callback)](#emitteremit-message-callback) 52 | - [emitter.on (topic, listener, [callback])](#emitteron-topic-listener-callback) 53 | - [emitter.removeListener (topic, listener, [callback])](#emitterremovelistener-topic-listener-callback) 54 | - [emitter.close (callback)](#emitterclose-callback) 55 | 56 | ## new MQEmitter ([options]) 57 | 58 | - options `` 59 | - `concurrency` `` maximum number of concurrent messages that can be on concurrent delivery. __Default__: `0` 60 | - `wildcardOne` `` a char to use for matching exactly one _non-empty_ level word. __Default__: `+` 61 | - `wildcardSome` `` a char to use for matching multiple level wildcards. __Default__: #` 62 | - `matchEmptyLevels` `` If true then `wildcardOne` also matches an empty word. __Default__: `true` 63 | - `separator` `` a separator character to use for separating words. __Default__: `/` 64 | 65 | Create a new MQEmitter class. 66 | 67 | MQEmitter is the class and function exposed by this module. 68 | It can be created by `MQEmitter()` or using `new MQEmitter()`. 69 | 70 | For more information on wildcards, see [this explanation](#wildcards) or [Qlobber](https://www.npmjs.com/qlobber). 71 | 72 | ## emitter.emit (message, callback) 73 | 74 | - `message` `` 75 | - `callback` `` `(error) => void` 76 | - error `` | `null` 77 | 78 | Emit the given message, which must have a `topic` property, which can contain wildcards as defined on creation. 79 | 80 | ## emitter.on (topic, listener, [callback]) 81 | 82 | - `topic` `` 83 | - `listener` `` `(message, done) => void` 84 | - `callback` `` `() => void` 85 | 86 | Add the given listener to the passed topic. Topic can contain wildcards, as defined on creation. 87 | 88 | The `listener` __must never error__ and `done` must not be called with an __`err`__ object. 89 | 90 | `callback` will be called when the event subscribe is done correctly. 91 | 92 | ## emitter.removeListener (topic, listener, [callback]) 93 | 94 | The inverse of `on`. 95 | 96 | ## emitter.close (callback) 97 | 98 | - `callback` `` `() => void` 99 | 100 | Close the given emitter. After, all writes will return an error. 101 | 102 | ## Wildcards 103 | 104 | __MQEmitter__ supports the use of wildcards: every topic is splitted according to `separator`. 105 | 106 | The wildcard character `+` matches exactly _non-empty_ one word: 107 | 108 | ```js 109 | const mq = require('mqemitter') 110 | const emitter = mq() 111 | 112 | emitter.on('hello/+/world', function(message, cb) { 113 | // will ONLY capture { topic: 'hello/my/world', 'something': 'more' } 114 | console.log(message) 115 | cb() 116 | }) 117 | emitter.on('hello/+', function(message, cb) { 118 | // will not be called 119 | console.log(message) 120 | cb() 121 | }) 122 | 123 | emitter.emit({ topic: 'hello/my/world', something: 'more' }) 124 | emitter.emit({ topic: 'hello//world', something: 'more' }) 125 | ``` 126 | 127 | The wildcard character `+` matches one word: 128 | 129 | ```js 130 | const mq = require('mqemitter') 131 | const emitter = mq({ matchEmptyLevels: true }) 132 | 133 | emitter.on('hello/+/world', function(message, cb) { 134 | // will capture { topic: 'hello/my/world', 'something': 'more' } 135 | // and capture { topic: 'hello//world', 'something': 'more' } 136 | console.log(message) 137 | cb() 138 | }) 139 | 140 | emitter.on('hello/+', function(message, cb) { 141 | // will not be called 142 | console.log(message) 143 | cb() 144 | }) 145 | 146 | emitter.emit({ topic: 'hello/my/world', something: 'more' }) 147 | emitter.emit({ topic: 'hello//world', something: 'more' }) 148 | ``` 149 | 150 | The wildcard character `#` matches zero or more words: 151 | 152 | ```js 153 | const mq = require('mqemitter') 154 | const emitter = mq() 155 | 156 | emitter.on('hello/#', function(message, cb) { 157 | // this will print { topic: 'hello/my/world', 'something': 'more' } 158 | console.log(message) 159 | cb() 160 | }) 161 | 162 | emitter.on('#', function(message, cb) { 163 | // this will print { topic: 'hello/my/world', 'something': 'more' } 164 | console.log(message) 165 | cb() 166 | }) 167 | 168 | emitter.on('hello/my/world/#', function(message, cb) { 169 | // this will print { topic: 'hello/my/world', 'something': 'more' } 170 | console.log(message) 171 | cb() 172 | }) 173 | 174 | emitter.emit({ topic: 'hello/my/world', something: 'more' }) 175 | ``` 176 | 177 | Of course, you can mix `#` and `+` in the same subscription. 178 | 179 | ## LICENSE 180 | 181 | MIT 182 | 183 | [mqemitter-redis]: https://www.npmjs.com/mqemitter-redis 184 | [mqemitter-mongodb]: https://www.npmjs.com/mqemitter-mongodb 185 | [mqemitter-child-process]: https://www.npmjs.com/mqemitter-child-process 186 | [mqemitter-cs]: https://www.npmjs.com/mqemitter-cs 187 | [mqemitter-p2p]: https://www.npmjs.com/mqemitter-p2p 188 | [mqemitter-aerospike]: https://www.npmjs.com/mqemitter-aerospike 189 | -------------------------------------------------------------------------------- /abstractTest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function abstractTests (opts) { 4 | const builder = opts.builder 5 | const test = opts.test 6 | 7 | test('support on and emit', async t => { 8 | t.plan(4) 9 | 10 | const e = builder() 11 | const expected = { 12 | topic: 'hello world', 13 | payload: { my: 'message' } 14 | } 15 | 16 | await new Promise(resolve => { 17 | e.on('hello world', function (message, cb) { 18 | t.assert.equal(e.current, 1, 'number of current messages') 19 | t.assert.deepEqual(message, expected) 20 | t.assert.equal(this, e) 21 | cb() 22 | }, () => { 23 | e.emit(expected, () => { 24 | e.close(() => { 25 | t.assert.ok(true, 'closed') 26 | resolve() 27 | }) 28 | }) 29 | }) 30 | }) 31 | }) 32 | 33 | test('support multiple subscribers', async t => { 34 | t.plan(3) 35 | 36 | const e = builder() 37 | const expected = { 38 | topic: 'hello world', 39 | payload: { my: 'message' } 40 | } 41 | 42 | await new Promise(resolve => { 43 | e.on('hello world', (message, cb) => { 44 | t.assert.ok(message, 'message received') 45 | cb() 46 | }, () => { 47 | e.on('hello world', (message, cb) => { 48 | t.assert.ok(message, 'message received') 49 | cb() 50 | }, () => { 51 | e.emit(expected, () => { 52 | e.close(() => { 53 | t.assert.ok(true, 'closed') 54 | resolve() 55 | }) 56 | }) 57 | }) 58 | }) 59 | }) 60 | }) 61 | 62 | test('support multiple subscribers and unsubscribers', async t => { 63 | t.plan(2) 64 | 65 | const e = builder() 66 | const expected = { 67 | topic: 'hello world', 68 | payload: { my: 'message' } 69 | } 70 | 71 | await new Promise(resolve => { 72 | function first (message, cb) { 73 | t.fail('first listener should not receive any events') 74 | cb() 75 | } 76 | 77 | function second (message, cb) { 78 | t.assert.ok(message, 'second listener must receive the message') 79 | cb() 80 | e.close(() => { 81 | t.assert.ok(true, 'closed') 82 | resolve() 83 | }) 84 | } 85 | 86 | e.on('hello world', first, () => { 87 | e.on('hello world', second, () => { 88 | e.removeListener('hello world', first, () => { 89 | e.emit(expected) 90 | }) 91 | }) 92 | }) 93 | }) 94 | }) 95 | 96 | test('removeListener', async t => { 97 | t.plan(1) 98 | 99 | const e = builder() 100 | const expected = { 101 | topic: 'hello world', 102 | payload: { my: 'message' } 103 | } 104 | let toRemoveCalled = false 105 | 106 | function toRemove (message, cb) { 107 | toRemoveCalled = true 108 | cb() 109 | } 110 | 111 | await new Promise(resolve => { 112 | e.on('hello world', (message, cb) => { 113 | cb() 114 | }, () => { 115 | e.on('hello world', toRemove, () => { 116 | e.removeListener('hello world', toRemove, () => { 117 | e.emit(expected, () => { 118 | e.close(() => { 119 | t.assert.ok(!toRemoveCalled, 'the toRemove function must not be called') 120 | resolve() 121 | }) 122 | }) 123 | }) 124 | }) 125 | }) 126 | }) 127 | }) 128 | 129 | test('without a callback on emit and on', async t => { 130 | t.plan(1) 131 | 132 | const e = builder() 133 | const expected = { 134 | topic: 'hello world', 135 | payload: { my: 'message' } 136 | } 137 | 138 | await new Promise(resolve => { 139 | e.on('hello world', (message, cb) => { 140 | cb() 141 | e.close(() => { 142 | t.assert.ok(true, 'closed') 143 | resolve() 144 | }) 145 | }) 146 | 147 | setTimeout(() => { 148 | e.emit(expected) 149 | }, 100) 150 | }) 151 | }) 152 | 153 | test('without any listeners', async t => { 154 | t.plan(2) 155 | 156 | const e = builder() 157 | const expected = { 158 | topic: 'hello world', 159 | payload: { my: 'message' } 160 | } 161 | 162 | await new Promise(resolve => { 163 | e.emit(expected) 164 | t.assert.equal(e.current, 0, 'reset the current messages trackers') 165 | e.close(() => { 166 | t.assert.ok(true, 'closed') 167 | resolve() 168 | }) 169 | }) 170 | }) 171 | 172 | test('support one level wildcard', async t => { 173 | t.plan(2) 174 | 175 | const e = builder() 176 | const expected = { 177 | topic: 'hello/world', 178 | payload: { my: 'message' } 179 | } 180 | 181 | await new Promise(resolve => { 182 | e.on('hello/+', (message, cb) => { 183 | t.assert.equal(message.topic, 'hello/world') 184 | cb() 185 | }, () => { 186 | // this will not be catched 187 | e.emit({ topic: 'hello/my/world' }) 188 | 189 | // this will be catched 190 | e.emit(expected, () => { 191 | e.close(() => { 192 | t.assert.ok(true, 'closed') 193 | resolve() 194 | }) 195 | }) 196 | }) 197 | }) 198 | }) 199 | 200 | test('support one level wildcard - not match empty words', async t => { 201 | t.plan(2) 202 | 203 | const e = builder({ matchEmptyLevels: false }) 204 | const expected = { 205 | topic: 'hello/dummy/world', 206 | payload: { my: 'message' } 207 | } 208 | 209 | await new Promise(resolve => { 210 | e.on('hello/+/world', (message, cb) => { 211 | t.assert.equal(message.topic, 'hello/dummy/world') 212 | cb() 213 | }, () => { 214 | // this will not be catched 215 | e.emit({ topic: 'hello//world' }) 216 | 217 | // this will be catched 218 | e.emit(expected, () => { 219 | e.close(() => { 220 | t.assert.ok(true, 'closed') 221 | resolve() 222 | }) 223 | }) 224 | }) 225 | }) 226 | }) 227 | 228 | test('support one level wildcard - match empty words', async t => { 229 | t.plan(3) 230 | 231 | const e = builder({ matchEmptyLevels: true }) 232 | 233 | await new Promise(resolve => { 234 | e.on('hello/+/world', (message, cb) => { 235 | const topic = message.topic 236 | if (topic === 'hello//world' || topic === 'hello/dummy/world') { 237 | t.assert.ok(true, `received ${topic}`) 238 | } 239 | cb() 240 | }, () => { 241 | // this will be catched 242 | e.emit({ topic: 'hello//world' }) 243 | // this will be catched 244 | e.emit({ topic: 'hello/dummy/world' }, () => { 245 | e.close(() => { 246 | t.assert.ok(true, 'closed') 247 | resolve() 248 | }) 249 | }) 250 | }) 251 | }) 252 | }) 253 | 254 | test('support one level wildcard - match empty words', async t => { 255 | t.plan(2) 256 | 257 | const e = builder({ matchEmptyLevels: true }) 258 | await new Promise(resolve => { 259 | e.on('hello/+', (message, cb) => { 260 | t.assert.equal(message.topic, 'hello/') 261 | cb() 262 | }, () => { 263 | // this will be catched 264 | e.emit({ topic: 'hello/' }, () => { 265 | e.close(() => { 266 | t.assert.ok(true, 'closed') 267 | resolve() 268 | }) 269 | }) 270 | }) 271 | }) 272 | }) 273 | 274 | test('support one level wildcard - not match empty words', async t => { 275 | t.plan(1) 276 | 277 | const e = builder({ matchEmptyLevels: false }) 278 | 279 | await new Promise(resolve => { 280 | e.on('hello/+', (message, cb) => { 281 | t.fail('should not catch') 282 | cb() 283 | }, () => { 284 | // this will not be catched 285 | e.emit({ topic: 'hello/' }, () => { 286 | e.close(() => { 287 | t.assert.ok(true, 'closed') 288 | resolve() 289 | }) 290 | }) 291 | }) 292 | }) 293 | }) 294 | 295 | test('support changing one level wildcard', async t => { 296 | t.plan(2) 297 | 298 | const e = builder({ wildcardOne: '~' }) 299 | const expected = { 300 | topic: 'hello/world', 301 | payload: { my: 'message' } 302 | } 303 | 304 | await new Promise(resolve => { 305 | e.on('hello/~', (message, cb) => { 306 | t.assert.equal(message.topic, 'hello/world') 307 | cb() 308 | }, () => { 309 | e.emit(expected, () => { 310 | e.close(() => { 311 | t.assert.ok(true, 'closed') 312 | resolve() 313 | }) 314 | }) 315 | }) 316 | }) 317 | }) 318 | 319 | test('support deep wildcard', async t => { 320 | t.plan(2) 321 | 322 | const e = builder() 323 | const expected = { 324 | topic: 'hello/my/world', 325 | payload: { my: 'message' } 326 | } 327 | await new Promise(resolve => { 328 | e.on('hello/#', (message, cb) => { 329 | t.assert.equal(message.topic, 'hello/my/world') 330 | cb() 331 | }, () => { 332 | e.emit(expected, () => { 333 | e.close(() => { 334 | t.assert.ok(true, 'closed') 335 | resolve() 336 | }) 337 | }) 338 | }) 339 | }) 340 | }) 341 | 342 | test('support deep wildcard without separator', async t => { 343 | t.plan(2) 344 | 345 | const e = builder() 346 | const expected = { 347 | topic: 'hello', 348 | payload: { my: 'message' } 349 | } 350 | 351 | await new Promise(resolve => { 352 | e.on('#', (message, cb) => { 353 | t.assert.equal(message.topic, expected.topic) 354 | cb() 355 | }, () => { 356 | e.emit(expected, () => { 357 | e.close(() => { 358 | t.assert.ok(true, 'closed') 359 | resolve() 360 | }) 361 | }) 362 | }) 363 | }) 364 | }) 365 | 366 | test('support deep wildcard - match empty words', async t => { 367 | t.plan(2) 368 | 369 | const e = builder({ matchEmptyLevels: true }) 370 | const expected = { 371 | topic: 'hello', 372 | payload: { my: 'message' } 373 | } 374 | 375 | const wrong = { 376 | topic: 'hellooo', 377 | payload: { my: 'message' } 378 | } 379 | 380 | await new Promise(resolve => { 381 | e.on('hello/#', (message, cb) => { 382 | t.assert.equal(message.topic, expected.topic) 383 | cb() 384 | }, () => { 385 | e.emit(wrong) // this should not be received 386 | e.emit(expected, () => { 387 | e.close(() => { 388 | t.assert.ok(true, 'closed') 389 | resolve() 390 | }) 391 | }) 392 | }) 393 | }) 394 | }) 395 | 396 | test('support changing deep wildcard', async t => { 397 | t.plan(2) 398 | 399 | const e = builder({ wildcardSome: '*' }) 400 | const expected = { 401 | topic: 'hello/my/world', 402 | payload: { my: 'message' } 403 | } 404 | 405 | await new Promise(resolve => { 406 | e.on('hello/*', (message, cb) => { 407 | t.assert.equal(message.topic, 'hello/my/world') 408 | cb() 409 | }, () => { 410 | e.emit(expected, () => { 411 | e.close(() => { 412 | t.assert.ok(true, 'closed') 413 | resolve() 414 | }) 415 | }) 416 | }) 417 | }) 418 | }) 419 | 420 | test('support changing the level separator', async t => { 421 | t.plan(2) 422 | 423 | const e = builder({ separator: '~' }) 424 | const expected = { 425 | topic: 'hello~world', 426 | payload: { my: 'message' } 427 | } 428 | 429 | await new Promise(resolve => { 430 | e.on('hello~+', (message, cb) => { 431 | t.assert.equal(message.topic, 'hello~world') 432 | cb() 433 | }, () => { 434 | e.emit(expected, () => { 435 | e.close(() => { 436 | t.assert.ok(true, 'closed') 437 | resolve() 438 | }) 439 | }) 440 | }) 441 | }) 442 | }) 443 | 444 | test('close support', async t => { 445 | const e = builder() 446 | let check = false 447 | 448 | t.assert.ok(!e.closed, 'must have a false closed property') 449 | 450 | await new Promise(resolve => { 451 | e.close(() => { 452 | t.assert.ok(check, 'must delay the close callback') 453 | t.assert.ok(e.closed, 'must have a true closed property') 454 | resolve() 455 | }) 456 | check = true 457 | }) 458 | }) 459 | 460 | test('emit after close errors', async t => { 461 | const e = builder() 462 | 463 | await new Promise(resolve => { 464 | e.close(() => { 465 | e.emit({ topic: 'hello' }, err => { 466 | t.assert.ok(err, 'must return an error') 467 | resolve() 468 | }) 469 | }) 470 | }) 471 | }) 472 | 473 | test('support multiple subscribers with wildcards', async t => { 474 | const e = builder() 475 | const expected = { 476 | topic: 'hello/world', 477 | payload: { my: 'message' } 478 | } 479 | let firstCalled = false 480 | let secondCalled = false 481 | 482 | await new Promise(resolve => { 483 | e.on('hello/#', (message, cb) => { 484 | t.assert.ok(!firstCalled, 'first subscriber must only be called once') 485 | firstCalled = true 486 | cb() 487 | }) 488 | 489 | e.on('hello/+', (message, cb) => { 490 | t.assert.ok(!secondCalled, 'second subscriber must only be called once') 491 | secondCalled = true 492 | cb() 493 | }, () => { 494 | e.emit(expected, () => { 495 | e.close(() => { 496 | resolve() 497 | }) 498 | }) 499 | }) 500 | }) 501 | }) 502 | 503 | test('support multiple subscribers with wildcards (deep)', async t => { 504 | const e = builder() 505 | const expected = { 506 | topic: 'hello/my/world', 507 | payload: { my: 'message' } 508 | } 509 | let firstCalled = false 510 | let secondCalled = false 511 | 512 | await new Promise(resolve => { 513 | e.on('hello/#', (message, cb) => { 514 | t.assert.ok(!firstCalled, 'first subscriber must only be called once') 515 | firstCalled = true 516 | cb() 517 | }) 518 | 519 | e.on('hello/+/world', (message, cb) => { 520 | t.assert.ok(!secondCalled, 'second subscriber must only be called once') 521 | secondCalled = true 522 | cb() 523 | }, () => { 524 | e.emit(expected, () => { 525 | e.close(() => { 526 | resolve() 527 | }) 528 | }) 529 | }) 530 | }) 531 | }) 532 | 533 | test('emit & receive buffers', async t => { 534 | const e = builder() 535 | const msg = Buffer.from('hello') 536 | const expected = { 537 | topic: 'hello', 538 | payload: msg 539 | } 540 | 541 | await new Promise(resolve => { 542 | e.on('hello', (message, cb) => { 543 | t.assert.deepEqual(msg, message.payload) 544 | cb() 545 | }, () => { 546 | e.emit(expected, () => { 547 | e.close(() => { 548 | resolve() 549 | }) 550 | }) 551 | }) 552 | }) 553 | }) 554 | 555 | test('packets are emitted in order', async t => { 556 | const e = builder() 557 | const total = 10000 558 | const topic = 'test' 559 | 560 | let received = 0 561 | 562 | await new Promise(resolve => { 563 | e.on(topic, (msg, cb) => { 564 | let fail = false 565 | if (received !== msg.payload) { 566 | t.fail(`leak detected. Count: ${received} - Payload: ${msg.payload}`) 567 | fail = true 568 | } 569 | 570 | received++ 571 | 572 | if (fail || received === total) { 573 | e.close(() => { 574 | resolve() 575 | }) 576 | } 577 | cb() 578 | }, () => { 579 | for (let payload = 0; payload < total; payload++) { 580 | e.emit({ topic, payload }) 581 | } 582 | }) 583 | }) 584 | }) 585 | 586 | test('calling emit without cb when closed doesn\'t throw error', async t => { 587 | const e = builder() 588 | const msg = Buffer.from('hello') 589 | const expected = { 590 | topic: 'hello', 591 | payload: msg 592 | } 593 | 594 | await new Promise(resolve => { 595 | e.close(() => { 596 | try { 597 | e.emit(expected) 598 | } catch (error) { 599 | t.assert.ifError('throws error') 600 | } 601 | resolve() 602 | }) 603 | }) 604 | }) 605 | } 606 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mqemitter = require('./') 4 | const emitter = mqemitter({ concurrency: 10 }) 5 | const total = 1000000 6 | let written = 0 7 | let received = 0 8 | const timerKey = 'time for sending ' + total + ' messages' 9 | 10 | function write () { 11 | if (written === total) { 12 | return 13 | } 14 | 15 | written++ 16 | 17 | emitter.emit({ topic: 'hello', payload: 'world' }, write) 18 | } 19 | 20 | emitter.on('hello', function (msg, cb) { 21 | received++ 22 | if (received === total) { 23 | console.timeEnd(timerKey) 24 | } 25 | setImmediate(cb) 26 | }) 27 | 28 | emitter.on('hello', (msg, cb) => { 29 | setImmediate(cb) 30 | }) 31 | 32 | console.time(timerKey) 33 | write() 34 | write() 35 | write() 36 | write() 37 | write() 38 | write() 39 | write() 40 | write() 41 | write() 42 | write() 43 | write() 44 | write() 45 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('neostandard')(({ ts: true })) 2 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mqemitter = require('.') 4 | 5 | const mq = mqemitter() 6 | 7 | function subscribe (topic, obj) { 8 | mq.on(topic, callback) 9 | obj.close = close 10 | 11 | function callback (value, cb) { 12 | obj.push(value) 13 | cb() 14 | } 15 | 16 | function close () { 17 | mq.removeListener(topic, callback) 18 | } 19 | } 20 | 21 | class MyQueue { 22 | push (value) { 23 | console.log(value) 24 | } 25 | } 26 | 27 | const a = new MyQueue() 28 | const b = new MyQueue() 29 | const c = new MyQueue() 30 | 31 | subscribe('hello', a) 32 | subscribe('hello', b) 33 | subscribe('hello', c) 34 | 35 | mq.emit({ topic: 'hello', payload: 'world' }) 36 | 37 | a.close() 38 | b.close() 39 | c.close() 40 | 41 | mq.emit({ topic: 'hello', payload: 'world' }) 42 | 43 | // const listeners = new Map() 44 | // 45 | // 46 | // const queues = new Map() 47 | // 48 | // function subscribe (topic, queue) { 49 | // if (listeners.has(topic)) { 50 | // 51 | // } 52 | // 53 | // function callback (err) { 54 | // 55 | // for (var value of queues) { 56 | // } 57 | // } 58 | // 59 | // listeners.set(topic, callback) 60 | // queues.set(topic, [queue]) 61 | // } 62 | -------------------------------------------------------------------------------- /mqemitter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Qlobber } = require('qlobber') 4 | const assert = require('assert') 5 | const fastparallel = require('fastparallel') 6 | 7 | function MQEmitter (opts) { 8 | if (!(this instanceof MQEmitter)) { 9 | return new MQEmitter(opts) 10 | } 11 | 12 | const that = this 13 | 14 | opts = opts || {} 15 | opts.matchEmptyLevels = opts.matchEmptyLevels === undefined ? true : !!opts.matchEmptyLevels 16 | opts.separator = opts.separator || '/' 17 | opts.wildcardOne = opts.wildcardOne || '+' 18 | opts.wildcardSome = opts.wildcardSome || '#' 19 | 20 | this._messageQueue = [] 21 | this._messageCallbacks = [] 22 | this._parallel = fastparallel({ 23 | results: false, 24 | released 25 | }) 26 | 27 | this.concurrency = opts.concurrency || 0 28 | 29 | this.current = 0 30 | this._doing = false 31 | this._matcher = new Qlobber({ 32 | match_empty_levels: opts.matchEmptyLevels, 33 | separator: opts.separator, 34 | wildcard_one: opts.wildcardOne, 35 | wildcard_some: opts.wildcardSome 36 | }) 37 | 38 | this.closed = false 39 | this._released = released 40 | 41 | function released () { 42 | that.current-- 43 | 44 | const message = that._messageQueue.shift() 45 | const callback = that._messageCallbacks.shift() 46 | 47 | if (message) { 48 | that._do(message, callback) 49 | } else { 50 | that._doing = false 51 | } 52 | } 53 | } 54 | 55 | Object.defineProperty(MQEmitter.prototype, 'length', { 56 | get: function () { 57 | return this._messageQueue.length 58 | }, 59 | enumerable: true 60 | }) 61 | 62 | MQEmitter.prototype.on = function on (topic, notify, done) { 63 | assert(topic) 64 | assert(notify) 65 | this._matcher.add(topic, notify) 66 | 67 | if (done) { 68 | setImmediate(done) 69 | } 70 | 71 | return this 72 | } 73 | 74 | MQEmitter.prototype.removeListener = function removeListener (topic, notify, done) { 75 | assert(topic) 76 | assert(notify) 77 | const that = this 78 | setImmediate(function () { 79 | that._matcher.remove(topic, notify) 80 | if (done) { 81 | done() 82 | } 83 | }) 84 | return this 85 | } 86 | 87 | MQEmitter.prototype.removeAllListeners = function removeListener (topic, done) { 88 | assert(topic) 89 | this._matcher.remove(topic) 90 | 91 | if (done) { 92 | setImmediate(done) 93 | } 94 | 95 | return this 96 | } 97 | 98 | MQEmitter.prototype.emit = function emit (message, cb) { 99 | assert(message) 100 | 101 | cb = cb || noop 102 | 103 | if (this.closed) { 104 | return cb(new Error('mqemitter is closed')) 105 | } 106 | 107 | if (this.concurrency > 0 && this.current >= this.concurrency) { 108 | this._messageQueue.push(message) 109 | this._messageCallbacks.push(cb) 110 | if (!this._doing) { 111 | process.emitWarning('MqEmitter leak detected', { detail: 'For more info check: https://github.com/mcollina/mqemitter/pull/94' }) 112 | this._released() 113 | } 114 | } else { 115 | this._do(message, cb) 116 | } 117 | 118 | return this 119 | } 120 | 121 | MQEmitter.prototype.close = function close (cb) { 122 | this.closed = true 123 | setImmediate(cb) 124 | 125 | return this 126 | } 127 | 128 | MQEmitter.prototype._do = function (message, callback) { 129 | this._doing = true 130 | const matches = this._matcher.match(message.topic) 131 | 132 | this.current++ 133 | this._parallel(this, matches, message, callback) 134 | 135 | return this 136 | } 137 | 138 | function noop () { } 139 | 140 | module.exports = MQEmitter 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqemitter", 3 | "version": "7.0.0", 4 | "description": "An Opinionated Message Queue with an emitter-style API", 5 | "main": "mqemitter.js", 6 | "types": "types/mqemitter.d.ts", 7 | "scripts": { 8 | "lint:fix": "eslint --fix", 9 | "lint": "npm run lint:standard && npm run lint:markdown", 10 | "lint:standard": "eslint", 11 | "lint:markdown": "markdownlint README.md", 12 | "unit": "node --test test/*.js", 13 | "unit:cov": "c8 --reporter=lcov npm run unit", 14 | "postunit:cov": "c8 check-coverage --lines 100 --functions 100 --branches 100", 15 | "typescript": "tsc --project ./test/types/tsconfig.json", 16 | "test:types": "tsd", 17 | "test": "npm run lint && npm run unit:cov && tsd && npm run typescript" 18 | }, 19 | "pre-commit": [ 20 | "test" 21 | ], 22 | "website": "https://github.com/mcollina/mqemitter", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/mcollina/mqemitter.git" 26 | }, 27 | "bugs": { 28 | "url": "http://github.com/mcollina/mqemitter/issues" 29 | }, 30 | "author": "Matteo Collina ", 31 | "engines": { 32 | "node": ">=20" 33 | }, 34 | "keywords": [ 35 | "emitter", 36 | "events", 37 | "message queue", 38 | "mq", 39 | "publish", 40 | "subscribe", 41 | "pub", 42 | "sub" 43 | ], 44 | "license": "ISC", 45 | "devDependencies": { 46 | "@fastify/pre-commit": "^2.2.0", 47 | "@types/node": "^22.13.14", 48 | "c8": "^10.1.3", 49 | "eslint": "^9.23.0", 50 | "markdownlint-cli": "^0.44.0", 51 | "neostandard": "^0.12.1", 52 | "tsd": "^0.31.2", 53 | "typescript": "^5.8.2" 54 | }, 55 | "dependencies": { 56 | "fastparallel": "^2.4.1", 57 | "qlobber": "^8.0.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const mq = require('../') 5 | 6 | require('../abstractTest')({ 7 | builder: mq, 8 | test 9 | }) 10 | 11 | test('queue concurrency', async t => { 12 | t.plan(3) 13 | 14 | await new Promise(resolve => { 15 | const e = mq({ concurrency: 1 }) 16 | let completed1 = false 17 | 18 | t.assert.equal(e.concurrency, 1) 19 | 20 | e.on('hello 1', (message, cb) => { 21 | setTimeout(cb, 10) 22 | }) 23 | 24 | e.on('hello 2', (message, cb) => { 25 | cb() 26 | }) 27 | 28 | e.emit({ topic: 'hello 1' }, () => { 29 | completed1 = true 30 | }) 31 | 32 | e.emit({ topic: 'hello 2' }, () => { 33 | t.assert.ok(completed1, 'the first message must be completed') 34 | resolve() 35 | }) 36 | t.assert.equal(e.length, 1) 37 | }) 38 | }) 39 | 40 | test('queue released when full', async t => { 41 | t.plan(21) 42 | 43 | await new Promise(resolve => { 44 | const e = mq({ concurrency: 1 }) 45 | 46 | e.on('hello 1', (message, cb) => { 47 | t.assert.ok(true, 'message received') 48 | setTimeout(cb, 10) 49 | }) 50 | 51 | function onSent () { 52 | t.assert.ok(true, 'message sent') 53 | } 54 | 55 | for (let i = 0; i < 9; i++) { 56 | e._messageQueue.push({ topic: 'hello 1' }) 57 | e._messageCallbacks.push(onSent) 58 | e.current++ 59 | } 60 | 61 | e.emit({ topic: 'hello 1' }, onSent) 62 | 63 | process.once('warning', warning => { 64 | t.assert.equal(warning.message, 'MqEmitter leak detected', 'warning message') 65 | }) 66 | setTimeout(resolve, 200) 67 | }) 68 | }) 69 | 70 | test('without any listeners and a callback', async t => { 71 | const e = mq() 72 | const expected = { 73 | topic: 'hello world', 74 | payload: { my: 'message' } 75 | } 76 | await new Promise(resolve => { 77 | e.emit(expected, () => { 78 | t.assert.equal(e.current, 1, 'there 1 message that is being processed') 79 | e.close(() => { 80 | resolve() 81 | }) 82 | }) 83 | }) 84 | }) 85 | 86 | test('queue concurrency with overlapping subscriptions', async t => { 87 | t.plan(3) 88 | 89 | const e = mq({ concurrency: 1 }) 90 | let completed1 = false 91 | 92 | await new Promise(resolve => { 93 | t.assert.equal(e.concurrency, 1) 94 | 95 | e.on('000001/021/#', (message, cb) => { 96 | setTimeout(cb, 10) 97 | }) 98 | 99 | e.on('000001/021/000B/0001/01', (message, cb) => { 100 | setTimeout(cb, 20) 101 | }) 102 | 103 | e.emit({ topic: '000001/021/000B/0001/01' }, () => { 104 | completed1 = true 105 | }) 106 | 107 | e.emit({ topic: '000001/021/000B/0001/01' }, () => { 108 | t.assert.ok(completed1, 'the first message must be completed') 109 | process.nextTick(() => { 110 | t.assert.equal(e.current, 0, 'no message is in flight') 111 | resolve() 112 | }) 113 | }) 114 | }) 115 | }) 116 | 117 | test('removeListener without a callback does not throw', t => { 118 | t.plan(1) 119 | const e = mq() 120 | function fn () {} 121 | 122 | e.on('hello', fn) 123 | e.removeListener('hello', fn) 124 | 125 | t.assert.ok(true, 'no error thrown') 126 | }) 127 | 128 | test('removeAllListeners removes listeners', async t => { 129 | t.plan(1) 130 | const e = mq() 131 | 132 | await new Promise(resolve => { 133 | e.on('hello', () => { 134 | t.fail('listener called') 135 | }) 136 | 137 | e.removeAllListeners('hello', () => { 138 | e.emit({ topic: 'hello' }, () => { 139 | t.assert.ok(true, 'no error thrown') 140 | resolve() 141 | }) 142 | }) 143 | }) 144 | }) 145 | 146 | test('removeAllListeners without a callback does not throw', t => { 147 | t.plan(1) 148 | const e = mq() 149 | function fn () {} 150 | 151 | e.on('hello', fn) 152 | e.removeAllListeners('hello') 153 | 154 | t.assert.ok(true, 'no error thrown') 155 | }) 156 | 157 | test('set defaults to opts', t => { 158 | t.plan(1) 159 | const opts = {} 160 | mq(opts) 161 | 162 | t.assert.deepEqual(opts, { 163 | matchEmptyLevels: true, 164 | separator: '/', 165 | wildcardOne: '+', 166 | wildcardSome: '#' 167 | }) 168 | }) 169 | 170 | test('removeListener inside messageHandler', t => { 171 | t.plan(3) 172 | 173 | const e = mq() 174 | 175 | function messageHandler1 (message, cb) { 176 | t.assert.ok(true, 'messageHandler1 called') 177 | // removes itself 178 | e.removeListener('hello', messageHandler1) 179 | cb() 180 | } 181 | 182 | e.on('hello', messageHandler1) 183 | 184 | function messageHandler2 (message, cb) { 185 | t.assert.ok(true, 'messageHandler2 called') 186 | cb() 187 | } 188 | 189 | e.on('hello', messageHandler2) 190 | 191 | e.emit({ topic: 'hello' }, () => { 192 | t.assert.ok(true, 'emit callback received') 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /test/types/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | /* eslint no-undef: 0 */ 3 | 4 | import MQEmitter, { Message } from '../../types/mqemitter' 5 | 6 | const noop = function () {} 7 | 8 | let mq = MQEmitter() 9 | mq = MQEmitter({ 10 | concurrency: 100 11 | }) 12 | mq.close(noop) 13 | 14 | mq = MQEmitter({ 15 | concurrency: 100, 16 | matchEmptyLevels: true, 17 | separator: ',' 18 | }) 19 | mq.close(noop) 20 | 21 | mq = MQEmitter({ 22 | concurrency: 10, 23 | matchEmptyLevels: true, 24 | separator: '/', 25 | wildcardOne: '+', 26 | wildcardSome: '#' 27 | }) 28 | 29 | const notify = function (msg: Message, cb: () => void) { 30 | if (msg.topic === 'hello/world') { 31 | console.log(msg) 32 | } 33 | cb() 34 | } 35 | 36 | mq.on('hello/+', notify) 37 | 38 | mq.emit({ topic: 'hello/world', payload: 'or any other fields', [Symbol.for('me')]: 42 }) 39 | 40 | mq.emit({ topic: 'hello/world' }, function (err) { 41 | console.log(err) 42 | }) 43 | 44 | mq.removeListener('hello/+', notify) 45 | 46 | mq.close(noop) 47 | -------------------------------------------------------------------------------- /test/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noEmit": true, 6 | "strict": true 7 | }, 8 | "files": [ 9 | "./index.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /types/mqemitter.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface MQEmitterOptions { 4 | concurrency?: number 5 | matchEmptyLevels?: boolean 6 | separator?: string 7 | wildcardOne?: string 8 | wildcardSome?: string 9 | } 10 | 11 | export type Message = Record & { topic: string } 12 | 13 | export interface MQEmitter { 14 | current: number 15 | concurrent: number 16 | on(topic: string, listener: (message: Message, done: () => void) => void, callback?: () => void): this 17 | emit(message: Message, callback?: (error?: Error) => void): void 18 | removeListener(topic: string, listener: (message: Message, done: () => void) => void, callback?: () => void): void 19 | close(callback: () => void): void 20 | } 21 | 22 | export default function (options?: MQEmitterOptions): MQEmitter 23 | -------------------------------------------------------------------------------- /types/mqemitter.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectError, expectType } from 'tsd' 2 | import mqEmitter from './mqemitter' 3 | import type { Message, MQEmitter } from './mqemitter' 4 | 5 | expectType(mqEmitter()) 6 | 7 | expectType(mqEmitter({ concurrency: 200, matchEmptyLevels: true })) 8 | 9 | expectType( 10 | mqEmitter({ 11 | concurrency: 10, 12 | matchEmptyLevels: true, 13 | separator: '/', 14 | wildcardOne: '+', 15 | wildcardSome: '#', 16 | }) 17 | ) 18 | 19 | function listener (message: Message, done: () => void) {} 20 | 21 | expectType(mqEmitter().on('topic', listener)) 22 | 23 | expectError(mqEmitter().emit(null)) 24 | 25 | expectType( 26 | mqEmitter().emit({ topic: 'test', prop1: 'prop1', [Symbol.for('me')]: 42 }) 27 | ) 28 | 29 | expectType(mqEmitter().emit({ topic: 'test', prop1: 'prop1' }, () => {})) 30 | 31 | expectType(mqEmitter().removeListener('topic', listener)) 32 | 33 | expectType(mqEmitter().close(() => null)) 34 | --------------------------------------------------------------------------------