首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: standard
versions:
- 16.0.3
+27
View File
@@ -0,0 +1,27 @@
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install
run: |
npm install
- name: Run tests
run: |
npm run test
+11
View File
@@ -0,0 +1,11 @@
{
"branches": 100,
"lines": 100,
"functions": 100,
"statements": 100,
"check-coverage": true,
"exclude": [
"abstractTest.js",
"test/*.js"
]
}
+84
View File
@@ -0,0 +1,84 @@
# Contributor Covenant Code of Conduct
## Our Pledge
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.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
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.
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.
## Scope
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.
## Enforcement
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.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**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.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**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.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**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.
### 4. Permanent Ban
**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.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2014-2020, Matteo Collina <hello@matteocollina.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+190
View File
@@ -0,0 +1,190 @@
<!-- markdownlint-disable MD013 MD024 -->
# MQEmitter
![ci](https://github.com/mcollina/mqemitter/workflows/ci/badge.svg)
[![Known Vulnerabilities](https://snyk.io/test/github/mcollina/mqemitter/badge.svg)](https://snyk.io/test/github/mcollina/mqemitter)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)\
[![Dependencies Status](https://david-dm.org/mcollina/mqemitter/status.svg)](https://david-dm.org/mcollina/mqemitter)
[![devDependencies Status](https://david-dm.org/mcollina/mqemitter/dev-status.svg)](https://david-dm.org/mcollina/mqemitter?type=dev)\
[![NPM version](https://img.shields.io/npm/v/mqemitter.svg?style=flat)](https://www.npmjs.com/mqemitter)
[![NPM downloads](https://img.shields.io/npm/dm/mqemitter.svg?style=flat)](https://www.npmjs.com/mqemitter)
An Opinionated Message Queue with an emitter-style API, but with callbacks.
If you need a multi process MQEmitter, check out the table below:
- [mqemitter-redis]: Redis-powered mqemitter
- [mqemitter-mongodb]: Mongodb based mqemitter
- [mqemitter-child-process]: Share the same mqemitter between a hierarchy of child processes
- [mqemitter-cs]: Expose a MQEmitter via a simple client/server protocol
- [mqemitter-p2p]: A P2P implementation of MQEmitter, based on HyperEmitter and a Merkle DAG
- [mqemitter-aerospike]: Aerospike mqemitter
## Installation
```sh
npm install mqemitter
```
## Examples
```js
const mq = require('mqemitter')
const emitter = mq({ concurrency: 5 })
const message
emitter.on('hello world', function (message, cb) {
// call callback when you are done
// do not pass any errors, the emitter cannot handle it.
cb()
})
// topic is mandatory
message = { topic: 'hello world', payload: 'or any other fields' }
emitter.emit(message, function () {
// emitter will never return an error
})
```
## API
- [new MQEmitter ([options])](#new-mqemitter-options)
- [emitter.emit (message, callback)](#emitteremit-message-callback)
- [emitter.on (topic, listener, [callback])](#emitteron-topic-listener-callback)
- [emitter.removeListener (topic, listener, [callback])](#emitterremovelistener-topic-listener-callback)
- [emitter.close (callback)](#emitterclose-callback)
## new MQEmitter ([options])
- options `<object>`
- `concurrency` `<number>` maximum number of concurrent messages that can be on concurrent delivery. __Default__: `0`
- `wildcardOne` `<string>` a char to use for matching exactly one _non-empty_ level word. __Default__: `+`
- `wildcardSome` `<string>` a char to use for matching multiple level wildcards. __Default__: #`
- `matchEmptyLevels` `<boolean>` If true then `wildcardOne` also matches an empty word. __Default__: `true`
- `separator` `<string>` a separator character to use for separating words. __Default__: `/`
Create a new MQEmitter class.
MQEmitter is the class and function exposed by this module.
It can be created by `MQEmitter()` or using `new MQEmitter()`.
For more information on wildcards, see [this explanation](#wildcards) or [Qlobber](https://www.npmjs.com/qlobber).
## emitter.emit (message, callback)
- `message` `<object>`
- `callback` `<Function>` `(error) => void`
- error `<Error>` | `null`
Emit the given message, which must have a `topic` property, which can contain wildcards as defined on creation.
## emitter.on (topic, listener, [callback])
- `topic` `<string>`
- `listener` `<Function>` `(message, done) => void`
- `callback` `<Function>` `() => void`
Add the given listener to the passed topic. Topic can contain wildcards, as defined on creation.
The `listener` __must never error__ and `done` must not be called with an __`err`__ object.
`callback` will be called when the event subscribe is done correctly.
## emitter.removeListener (topic, listener, [callback])
The inverse of `on`.
## emitter.close (callback)
- `callback` `<Function>` `() => void`
Close the given emitter. After, all writes will return an error.
## Wildcards
__MQEmitter__ supports the use of wildcards: every topic is splitted according to `separator`.
The wildcard character `+` matches exactly _non-empty_ one word:
```js
const mq = require('mqemitter')
const emitter = mq()
emitter.on('hello/+/world', function(message, cb) {
// will ONLY capture { topic: 'hello/my/world', 'something': 'more' }
console.log(message)
cb()
})
emitter.on('hello/+', function(message, cb) {
// will not be called
console.log(message)
cb()
})
emitter.emit({ topic: 'hello/my/world', something: 'more' })
emitter.emit({ topic: 'hello//world', something: 'more' })
```
The wildcard character `+` matches one word:
```js
const mq = require('mqemitter')
const emitter = mq({ matchEmptyLevels: true })
emitter.on('hello/+/world', function(message, cb) {
// will capture { topic: 'hello/my/world', 'something': 'more' }
// and capture { topic: 'hello//world', 'something': 'more' }
console.log(message)
cb()
})
emitter.on('hello/+', function(message, cb) {
// will not be called
console.log(message)
cb()
})
emitter.emit({ topic: 'hello/my/world', something: 'more' })
emitter.emit({ topic: 'hello//world', something: 'more' })
```
The wildcard character `#` matches zero or more words:
```js
const mq = require('mqemitter')
const emitter = mq()
emitter.on('hello/#', function(message, cb) {
// this will print { topic: 'hello/my/world', 'something': 'more' }
console.log(message)
cb()
})
emitter.on('#', function(message, cb) {
// this will print { topic: 'hello/my/world', 'something': 'more' }
console.log(message)
cb()
})
emitter.on('hello/my/world/#', function(message, cb) {
// this will print { topic: 'hello/my/world', 'something': 'more' }
console.log(message)
cb()
})
emitter.emit({ topic: 'hello/my/world', something: 'more' })
```
Of course, you can mix `#` and `+` in the same subscription.
## LICENSE
MIT
[mqemitter-redis]: https://www.npmjs.com/mqemitter-redis
[mqemitter-mongodb]: https://www.npmjs.com/mqemitter-mongodb
[mqemitter-child-process]: https://www.npmjs.com/mqemitter-child-process
[mqemitter-cs]: https://www.npmjs.com/mqemitter-cs
[mqemitter-p2p]: https://www.npmjs.com/mqemitter-p2p
[mqemitter-aerospike]: https://www.npmjs.com/mqemitter-aerospike
+543
View File
@@ -0,0 +1,543 @@
'use strict'
module.exports = function abstractTests (opts) {
const builder = opts.builder
const test = opts.test
test('support on and emit', function (t) {
t.plan(4)
const e = builder()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
e.on('hello world', function (message, cb) {
t.equal(e.current, 1, 'number of current messages')
t.deepEqual(message, expected)
t.equal(this, e)
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support multiple subscribers', function (t) {
t.plan(3)
const e = builder()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
e.on('hello world', function (message, cb) {
t.ok(message, 'message received')
cb()
}, function () {
e.on('hello world', function (message, cb) {
t.ok(message, 'message received')
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
})
test('support multiple subscribers and unsubscribers', function (t) {
t.plan(2)
const e = builder()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
function first (message, cb) {
t.fail('first listener should not receive any events')
cb()
}
function second (message, cb) {
t.ok(message, 'second listener must receive the message')
cb()
e.close(function () {
t.pass('closed')
})
}
e.on('hello world', first, function () {
e.on('hello world', second, function () {
e.removeListener('hello world', first, function () {
e.emit(expected)
})
})
})
})
test('removeListener', function (t) {
t.plan(1)
const e = builder()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
let toRemoveCalled = false
function toRemove (message, cb) {
toRemoveCalled = true
cb()
}
e.on('hello world', function (message, cb) {
cb()
}, function () {
e.on('hello world', toRemove, function () {
e.removeListener('hello world', toRemove, function () {
e.emit(expected, function () {
e.close(function () {
t.notOk(toRemoveCalled, 'the toRemove function must not be called')
})
})
})
})
})
})
test('without a callback on emit and on', function (t) {
t.plan(1)
const e = builder()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
e.on('hello world', function (message, cb) {
cb()
e.close(function () {
t.pass('closed')
})
})
setTimeout(function () {
e.emit(expected)
}, 100)
})
test('without any listeners', function (t) {
t.plan(2)
const e = builder()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
e.emit(expected)
t.equal(e.current, 0, 'reset the current messages trackers')
e.close(function () {
t.pass('closed')
})
})
test('support one level wildcard', function (t) {
t.plan(2)
const e = builder()
const expected = {
topic: 'hello/world',
payload: { my: 'message' }
}
e.on('hello/+', function (message, cb) {
t.equal(message.topic, 'hello/world')
cb()
}, function () {
// this will not be catched
e.emit({ topic: 'hello/my/world' })
// this will be catched
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support one level wildcard - not match empty words', function (t) {
t.plan(2)
const e = builder({ matchEmptyLevels: false })
const expected = {
topic: 'hello/dummy/world',
payload: { my: 'message' }
}
e.on('hello/+/world', function (message, cb) {
t.equal(message.topic, 'hello/dummy/world')
cb()
}, function () {
// this will not be catched
e.emit({ topic: 'hello//world' })
// this will be catched
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support one level wildcard - match empty words', function (t) {
t.plan(3)
const e = builder({ matchEmptyLevels: true })
e.on('hello/+/world', function (message, cb) {
const topic = message.topic
if (topic === 'hello//world' || topic === 'hello/dummy/world') {
t.pass('received ' + topic)
}
cb()
}, function () {
// this will be catched
e.emit({ topic: 'hello//world' })
// this will be catched
e.emit({ topic: 'hello/dummy/world' }, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support one level wildcard - match empty words', function (t) {
t.plan(2)
const e = builder({ matchEmptyLevels: true })
e.on('hello/+', function (message, cb) {
t.equal(message.topic, 'hello/')
cb()
}, function () {
// this will be catched
e.emit({ topic: 'hello/' }, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support one level wildcard - not match empty words', function (t) {
t.plan(1)
const e = builder({ matchEmptyLevels: false })
e.on('hello/+', function (message, cb) {
t.fail('should not catch')
cb()
}, function () {
// this will not be catched
e.emit({ topic: 'hello/' }, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support changing one level wildcard', function (t) {
t.plan(2)
const e = builder({ wildcardOne: '~' })
const expected = {
topic: 'hello/world',
payload: { my: 'message' }
}
e.on('hello/~', function (message, cb) {
t.equal(message.topic, 'hello/world')
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support deep wildcard', function (t) {
t.plan(2)
const e = builder()
const expected = {
topic: 'hello/my/world',
payload: { my: 'message' }
}
e.on('hello/#', function (message, cb) {
t.equal(message.topic, 'hello/my/world')
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support deep wildcard without separator', function (t) {
t.plan(2)
const e = builder()
const expected = {
topic: 'hello',
payload: { my: 'message' }
}
e.on('#', function (message, cb) {
t.equal(message.topic, expected.topic)
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support deep wildcard - match empty words', function (t) {
t.plan(2)
const e = builder({ matchEmptyLevels: true })
const expected = {
topic: 'hello',
payload: { my: 'message' }
}
const wrong = {
topic: 'hellooo',
payload: { my: 'message' }
}
e.on('hello/#', function (message, cb) {
t.equal(message.topic, expected.topic)
cb()
}, function () {
e.emit(wrong) // this should not be received
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support changing deep wildcard', function (t) {
t.plan(2)
const e = builder({ wildcardSome: '*' })
const expected = {
topic: 'hello/my/world',
payload: { my: 'message' }
}
e.on('hello/*', function (message, cb) {
t.equal(message.topic, 'hello/my/world')
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('support changing the level separator', function (t) {
t.plan(2)
const e = builder({ separator: '~' })
const expected = {
topic: 'hello~world',
payload: { my: 'message' }
}
e.on('hello~+', function (message, cb) {
t.equal(message.topic, 'hello~world')
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.pass('closed')
})
})
})
})
test('close support', function (t) {
const e = builder()
let check = false
t.notOk(e.closed, 'must have a false closed property')
e.close(function () {
t.ok(check, 'must delay the close callback')
t.ok(e.closed, 'must have a true closed property')
t.end()
})
check = true
})
test('emit after close errors', function (t) {
const e = builder()
e.close(function () {
e.emit({ topic: 'hello' }, function (err) {
t.ok(err, 'must return an error')
t.end()
})
})
})
test('support multiple subscribers with wildcards', function (t) {
const e = builder()
const expected = {
topic: 'hello/world',
payload: { my: 'message' }
}
let firstCalled = false
let secondCalled = false
e.on('hello/#', function (message, cb) {
t.notOk(firstCalled, 'first subscriber must only be called once')
firstCalled = true
cb()
})
e.on('hello/+', function (message, cb) {
t.notOk(secondCalled, 'second subscriber must only be called once')
secondCalled = true
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.end()
})
})
})
})
test('support multiple subscribers with wildcards (deep)', function (t) {
const e = builder()
const expected = {
topic: 'hello/my/world',
payload: { my: 'message' }
}
let firstCalled = false
let secondCalled = false
e.on('hello/#', function (message, cb) {
t.notOk(firstCalled, 'first subscriber must only be called once')
firstCalled = true
cb()
})
e.on('hello/+/world', function (message, cb) {
t.notOk(secondCalled, 'second subscriber must only be called once')
secondCalled = true
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.end()
})
})
})
})
test('emit & receive buffers', function (t) {
const e = builder()
const msg = Buffer.from('hello')
const expected = {
topic: 'hello',
payload: msg
}
e.on('hello', function (message, cb) {
t.deepEqual(msg, message.payload)
cb()
}, function () {
e.emit(expected, function () {
e.close(function () {
t.end()
})
})
})
})
test('packets are emitted in order', function (t) {
const e = builder()
const total = 10000
const topic = 'test'
let received = 0
e.on(topic, function (msg, cb) {
let fail = false
if (received !== msg.payload) {
t.fail(`leak detected. Count: ${received} - Payload: ${msg.payload}`)
fail = true
}
received++
if (fail || received === total) {
e.close(function () {
t.end()
})
}
cb()
}, function () {
for (let payload = 0; payload < total; payload++) {
e.emit({ topic, payload })
}
})
})
test('calling emit without cb when closed doesn\'t throw error', function (t) {
const e = builder()
const msg = Buffer.from('hello')
const expected = {
topic: 'hello',
payload: msg
}
e.close(function () {
try {
e.emit(expected)
} catch (error) {
t.error('throws error')
}
t.end()
})
})
}
+40
View File
@@ -0,0 +1,40 @@
'use strict'
const mqemitter = require('./')
const emitter = mqemitter({ concurrency: 10 })
const total = 1000000
let written = 0
let received = 0
const timerKey = 'time for sending ' + total + ' messages'
function write () {
if (written === total) {
return
}
written++
emitter.emit({ topic: 'hello', payload: 'world' }, write)
}
emitter.on('hello', function (msg, cb) {
received++
if (received === total) {
console.timeEnd(timerKey)
}
setImmediate(cb)
})
console.time(timerKey)
write()
write()
write()
write()
write()
write()
write()
write()
write()
write()
write()
write()
+61
View File
@@ -0,0 +1,61 @@
'use strict'
const mqemitter = require('.')
const mq = mqemitter()
function subscribe (topic, obj) {
mq.on(topic, callback)
obj.close = close
function callback (value, cb) {
obj.push(value)
cb()
}
function close () {
mq.removeListener(topic, callback)
}
}
class MyQueue {
push (value) {
console.log(value)
}
}
const a = new MyQueue()
const b = new MyQueue()
const c = new MyQueue()
subscribe('hello', a)
subscribe('hello', b)
subscribe('hello', c)
mq.emit({ topic: 'hello', payload: 'world' })
a.close()
b.close()
c.close()
mq.emit({ topic: 'hello', payload: 'world' })
// const listeners = new Map()
//
//
// const queues = new Map()
//
// function subscribe (topic, queue) {
// if (listeners.has(topic)) {
//
// }
//
// function callback (err) {
//
// for (var value of queues) {
// }
// }
//
// listeners.set(topic, callback)
// queues.set(topic, [queue])
// }
+140
View File
@@ -0,0 +1,140 @@
'use strict'
const { Qlobber } = require('qlobber')
const assert = require('assert')
const fastparallel = require('fastparallel')
function MQEmitter (opts) {
if (!(this instanceof MQEmitter)) {
return new MQEmitter(opts)
}
const that = this
opts = opts || {}
opts.matchEmptyLevels = opts.matchEmptyLevels === undefined ? true : !!opts.matchEmptyLevels
opts.separator = opts.separator || '/'
opts.wildcardOne = opts.wildcardOne || '+'
opts.wildcardSome = opts.wildcardSome || '#'
this._messageQueue = []
this._messageCallbacks = []
this._parallel = fastparallel({
results: false,
released
})
this.concurrency = opts.concurrency || 0
this.current = 0
this._doing = false
this._matcher = new Qlobber({
match_empty_levels: opts.matchEmptyLevels,
separator: opts.separator,
wildcard_one: opts.wildcardOne,
wildcard_some: opts.wildcardSome
})
this.closed = false
this._released = released
function released () {
that.current--
const message = that._messageQueue.shift()
const callback = that._messageCallbacks.shift()
if (message) {
that._do(message, callback)
} else {
that._doing = false
}
}
}
Object.defineProperty(MQEmitter.prototype, 'length', {
get: function () {
return this._messageQueue.length
},
enumerable: true
})
MQEmitter.prototype.on = function on (topic, notify, done) {
assert(topic)
assert(notify)
this._matcher.add(topic, notify)
if (done) {
setImmediate(done)
}
return this
}
MQEmitter.prototype.removeListener = function removeListener (topic, notify, done) {
assert(topic)
assert(notify)
const that = this
setImmediate(function () {
that._matcher.remove(topic, notify)
if (done) {
done()
}
})
return this
}
MQEmitter.prototype.removeAllListeners = function removeListener (topic, done) {
assert(topic)
this._matcher.remove(topic)
if (done) {
setImmediate(done)
}
return this
}
MQEmitter.prototype.emit = function emit (message, cb) {
assert(message)
cb = cb || noop
if (this.closed) {
return cb(new Error('mqemitter is closed'))
}
if (this.concurrency > 0 && this.current >= this.concurrency) {
this._messageQueue.push(message)
this._messageCallbacks.push(cb)
if (!this._doing) {
process.emitWarning('MqEmitter leak detected', { detail: 'For more info check: https://github.com/mcollina/mqemitter/pull/94' })
this._released()
}
} else {
this._do(message, cb)
}
return this
}
MQEmitter.prototype.close = function close (cb) {
this.closed = true
setImmediate(cb)
return this
}
MQEmitter.prototype._do = function (message, callback) {
this._doing = true
const matches = this._matcher.match(message.topic)
this.current++
this._parallel(this, matches, message, callback)
return this
}
function noop () { }
module.exports = MQEmitter
@@ -0,0 +1,46 @@
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Update APT
run: sudo apt update
- name: Install OS dependencies
run: sudo apt install libboost-dev libboost-context-dev
- name: Use Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install packages
run: npm install
- name: Lint
run: npx grunt lint
- name: Test
if: matrix.node-version != '20'
run: npm test
- name: Coverage
if: matrix.node-version == '20'
run: npm run coverage
- name: Coveralls
if: matrix.node-version == '20'
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+70
View File
@@ -0,0 +1,70 @@
/*jslint node: true */
"use strict";
const c8 = "npx c8 -x Gruntfile.js -x 'test/**'";
module.exports = function (grunt)
{
grunt.initConfig(
{
jshint: {
all: [ 'Gruntfile.js', 'index.js', 'lib/*.js', 'aedes/**/*.js', 'test/*.js', 'bench/**/*.js' ],
options: {
esversion: 11,
node: true
}
},
apidox: {
input: 'lib/qlobber.js',
output: 'README.md',
fullSourceDescription: true,
extraHeadingLevels: 1
},
exec: Object.fromEntries(Object.entries({
test: {
cmd: 'node --expose-gc ./node_modules/.bin/mocha test/*.js'
},
cover: {
cmd: `${c8} npx grunt test`
},
cover_report: {
cmd: `${c8} report -r lcov`
},
cover_check: {
cmd: `${c8} check-coverage --statements 100 --branches 100 --functions 100 --lines 100`
},
bench: {
cmd: 'npx bench -c 20000 -i bench/options/default.js,bench/options/dedup.js,bench/options/mapval.js,bench/options/default-native.js,bench/options/dedup-native.js,bench/options/default-cache-splits.js -k options bench/add bench/add_match_remove bench/match bench/match_search bench/test'
},
'bench-check': {
cmd: 'npx bench -c 20000 -i bench/options/check-default.js,bench/options/check-dedup.js,bench/options/check-mapval.js,bench/options/check-default-native.js,bench/options/check-dedup-native.js -k options bench/add bench/add_match_remove bench/match bench/match_search bench/test'
},
'bench-many': {
cmd: 'npx bench -c 1 -i bench/options/default.js,bench/options/dedup.js,bench/options/mapval.js,bench/options/default-native.js,bench/options/dedup-native.js,bench/options/default-cache-splits.js -k options bench/add_many bench/add_shortcut_many bench/match_many bench/match_search_many bench/test_many'
}
}).map(([k, v]) => [k, { stdio: 'inherit', ...v }]))
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-apidox');
grunt.loadNpmTasks('grunt-exec');
grunt.registerTask('lint', 'jshint');
grunt.registerTask('test', 'exec:test');
grunt.registerTask('docs', 'apidox');
grunt.registerTask('coverage', ['exec:cover',
'exec:cover_report',
'exec:cover_check']);
grunt.registerTask('bench', ['exec:bench',
'exec:bench-many']);
grunt.registerTask('bench-check', 'exec:bench-check');
grunt.registerTask('default', ['lint', 'test']);
};
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2016 David Halls <https://github.com/davedoesdev/>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+517
View File
@@ -0,0 +1,517 @@
# qlobber&nbsp;&nbsp;&nbsp;[![ci](https://github.com/davedoesdev/qlobber/actions/workflows/ci.yml/badge.svg)](https://github.com/davedoesdev/qlobber/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/davedoesdev/qlobber/badge.png?branch=master)](https://coveralls.io/r/davedoesdev/qlobber?branch=master) [![NPM version](https://badge.fury.io/js/qlobber.png)](http://badge.fury.io/js/qlobber)
Node.js globbing for amqp-like topics.
__Note:__ Version 5.0.0 adds async and worker thread support when used on Node 12+.
Example:
```javascript
var assert = require('assert');
var Qlobber = require('qlobber').Qlobber;
var matcher = new Qlobber();
matcher.add('foo.*', 'it matched!');
assert.deepEqual(matcher.match('foo.bar'), ['it matched!']);
assert(matcher.test('foo.bar', 'it matched!'));
```
The API is described [here](#tableofcontents).
qlobber is implemented using a trie, as described in the RabbitMQ blog posts [here](http://www.rabbitmq.com/blog/2010/09/14/very-fast-and-scalable-topic-routing-part-1/) and [here](http://www.rabbitmq.com/blog/2011/03/28/very-fast-and-scalable-topic-routing-part-2/).
## Installation
```shell
npm install qlobber
```
## Another Example
A more advanced example using topics from the [RabbitMQ topic tutorial](http://www.rabbitmq.com/tutorials/tutorial-five-python.html):
```javascript
var assert = require('assert');
var Qlobber = require('qlobber').Qlobber;
var matcher = new Qlobber();
matcher.add('*.orange.*', 'Q1');
matcher.add('*.*.rabbit', 'Q2');
matcher.add('lazy.#', 'Q2');
assert.deepEqual(['quick.orange.rabbit',
'lazy.orange.elephant',
'quick.orange.fox',
'lazy.brown.fox',
'lazy.pink.rabbit',
'quick.brown.fox',
'orange',
'quick.orange.male.rabbit',
'lazy.orange.male.rabbit'].map(function (topic)
{
return matcher.match(topic).sort();
}),
[['Q1', 'Q2'],
['Q1', 'Q2'],
['Q1'],
['Q2'],
['Q2', 'Q2'],
[],
[],
[],
['Q2']]);
```
## Async Example
Same as the first example but using `await`:
```javascript
const assert = require('assert');
const { Qlobber } = require('qlobber').set_native(require('qlobber-native'));
const matcher = new Qlobber.nativeString();
(async () => {
await matcher.addP('foo.*', 'it matched!');
assert.deepEqual(await matcher.matchP('foo.bar'), ['it matched!']);
assert(await matcher.testP('foo.bar', 'it matched!'));
})();
```
## Worker Thread Example
Same again but the matching is done on a separate thread:
```
const { Qlobber } = require('qlobber').set_native(require('qlobber-native'));
const {
Worker, isMainThread, parentPort, workerData
} = require('worker_threads');
if (isMainThread) {
const matcher = new Qlobber.nativeString();
matcher.add('foo.*', 'it matched!');
const worker = new Worker(__filename, {
workerData: matcher.state_address
});
worker.on('message', msg => {
const assert = require('assert');
assert.deepEqual(msg, [['it matched!'], true]);
});
} else {
const matcher = new Qlobber.nativeString(workerData);
parentPort.postMessage([
matcher.match('foo.bar'),
matcher.test('foo.bar', 'it matched!')
]);
}
```
## Licence
[MIT](LICENCE)
## Tests
qlobber passes the [RabbitMQ topic tests](https://github.com/rabbitmq/rabbitmq-server/blob/master/src/rabbit_tests.erl) (I converted them from Erlang to Javascript).
To run the tests:
```shell
npm test
```
## Lint
```shell
grunt lint
```
## Code Coverage
```shell
npm run coverage
```
[c8](https://github.com/bcoe/c8) results are available [here](http://rawgit.davedoesdev.com/davedoesdev/qlobber/master/coverage/lcov-report/index.html).
Coveralls page is [here](https://coveralls.io/r/davedoesdev/qlobber).
## Benchmarks
```shell
grunt bench
```
qlobber is also benchmarked in [ascoltatori](https://github.com/mcollina/ascoltatori).
## Native Qlobbers
The Javascript Qlobbers don't support asynchronous calls and worker threads
because Javascript values can't be shared between threads.
In order to support asynchronous calls and worker threads, a native C++
implementation is available in the
[qlobber-native](https://www.npmjs.com/package/qlobber-native) module.
Add qlobber-native as a dependency to your project and then add it to qlobber
like this:
```javascript
require('qlobber').set_native(require('qlobber-native'));
```
Note that [`set_native`](#set_nativeqlobber_native) returns qlobber's exports so you can do something like
this:
```javascript
const { Qlobber } = require('qlobber').set_native(require('qlobber-native'));
```
Note that qlobber-native requires Gnu C++ version 9+ and Boost 1.70+,
including the `boost_context` runtime library.
Once's you've added it to qlobber, the following classes will be available
alongside the Javascript classes:
- `Qlobber.nativeString`
- `Qlobber.nativeNumber`
- `QlobberDedup.nativeString`
- `QlobberDedup.nativeNumber`
- `QlobberTrue.native`
They can only hold values of a single type (currently strings or numbers).
### Asynchronous calls
The native classes support the same API as the Javascript classes but have the
following additional methods:
- `addP`
- `removeP`
- `matchP`
- `match_iterP`
- `testP`
- `clearP`
- `visitP`
- `get_restorerP`
They correspond to their namesakes but return Promises. Note that `match_iterP`
and `visitP` return async iterators.
# API
_Source: [lib/qlobber.js](lib/qlobber.js)_
<a name="tableofcontents"></a>
- <a name="toc_qlobberoptions"></a>[Qlobber](#qlobberoptions)
- <a name="toc_qlobberprototypeaddtopic-val"></a><a name="toc_qlobberprototype"></a>[Qlobber.prototype.add](#qlobberprototypeaddtopic-val)
- <a name="toc_qlobberprototyperemovetopic-val"></a>[Qlobber.prototype.remove](#qlobberprototyperemovetopic-val)
- <a name="toc_qlobberprototypematchtopic"></a>[Qlobber.prototype.match](#qlobberprototypematchtopic)
- <a name="toc_qlobberprototypematch_iter"></a>[Qlobber.prototype.match_iter](#qlobberprototypematch_iter)
- <a name="toc_qlobberprototypetesttopic-val"></a>[Qlobber.prototype.test](#qlobberprototypetesttopic-val)
- <a name="toc_qlobberprototypetest_valuesvals-val"></a>[Qlobber.prototype.test_values](#qlobberprototypetest_valuesvals-val)
- <a name="toc_qlobberprototypeclear"></a>[Qlobber.prototype.clear](#qlobberprototypeclear)
- <a name="toc_qlobberprototypevisit"></a>[Qlobber.prototype.visit](#qlobberprototypevisit)
- <a name="toc_qlobberprototypeget_restoreroptions"></a>[Qlobber.prototype.get_restorer](#qlobberprototypeget_restoreroptions)
- <a name="toc_qlobberdedupoptions"></a>[QlobberDedup](#qlobberdedupoptions)
- <a name="toc_qlobberdedupprototypetest_valuesvals-val"></a><a name="toc_qlobberdedupprototype"></a>[QlobberDedup.prototype.test_values](#qlobberdedupprototypetest_valuesvals-val)
- <a name="toc_qlobberdedupprototypematchtopic"></a>[QlobberDedup.prototype.match](#qlobberdedupprototypematchtopic)
- <a name="toc_qlobbertrueoptions"></a>[QlobberTrue](#qlobbertrueoptions)
- <a name="toc_qlobbertrueprototypetest_values"></a><a name="toc_qlobbertrueprototype"></a>[QlobberTrue.prototype.test_values](#qlobbertrueprototypetest_values)
- <a name="toc_qlobbertrueprototypematchtopic"></a>[QlobberTrue.prototype.match](#qlobbertrueprototypematchtopic)
- <a name="toc_visitorstreamqlobber"></a>[VisitorStream](#visitorstreamqlobber)
- <a name="toc_restorerstreamqlobber"></a>[RestorerStream](#restorerstreamqlobber)
- <a name="toc_set_nativeqlobber_native"></a>[set_native](#set_nativeqlobber_native)
## Qlobber([options])
> Creates a new qlobber.
**Parameters:**
- `{Object} [options]` Configures the qlobber. Use the following properties:
- `{String} separator` The character to use for separating words in topics. Defaults to '.'. MQTT uses '/' as the separator, for example.
- `{String} wildcard_one` The character to use for matching exactly one _non-empty_ word in a topic. Defaults to '*'. MQTT uses '+', for example.
- `{String} wildcard_some` The character to use for matching zero or more words in a topic. Defaults to '#'. MQTT uses '#' too.
- `{Boolean} match_empty_levels` If `true` then `wilcard_one` also matches an empty word in a topic. Defaults to `false`.
- `{Boolean|Map} cache_adds` Whether to cache topics when adding topic matchers. This will make adding multiple matchers for the same topic faster at the cost of extra memory usage. Defaults to `false`. If you supply a `Map` then it will be used to cache the topics (use this to enumerate all the topics in the qlobber).
- `{Integer} cache_splits` How many `topic.split` results to cache. When you pass in a topic, it has to be split on the `separator`. Caching the results will make using the same topics multiple times faster at the cost of extra memory usage. Defaults to `0` (no caching). The number of split results cached is limited by the value you pass here.
- `{Integer} max_words` Maximum number of words to allow in a topic. Defaults to 100.
- `{Integer} max_wildcard_somes` Maximum number of `wildcard_some` words in a topic. Defaults to 3.
<sub>Go: [TOC](#tableofcontents)</sub>
<a name="qlobberprototype"></a>
## Qlobber.prototype.add(topic, val)
> Add a topic matcher to the qlobber.
Note you can match more than one value against a topic by calling `add` multiple times with the same topic and different values.
**Parameters:**
- `{String} topic` The topic to match against.
- `{Any} val` The value to return if the topic is matched.
**Return:**
`{Qlobber}` The qlobber (for chaining).
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.remove(topic, [val])
> Remove a topic matcher from the qlobber.
**Parameters:**
- `{String} topic` The topic that's being matched against.
- `{Any} [val]` The value that's being matched. If you don't specify `val` then all matchers for `topic` are removed.
**Return:**
`{Qlobber}` The qlobber (for chaining).
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.match(topic)
> Match a topic.
**Parameters:**
- `{String} topic` The topic to match against.
**Return:**
`{Array}` List of values that matched the topic. This may contain duplicates. Use a [`QlobberDedup`](#qlobberdedupoptions) if you don't want duplicates.
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.match_iter()
> Match a topic, returning the matches one at a time.
**Return:**
`{Iterator}` An iterator on the values that match the topic. There may be duplicate values, even if you use a [`QlobberDedup`](#qlobberdedupoptions).
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.test(topic, val)
> Test whether a topic match contains a value. Faster than calling [`match`](#qlobberprototypematchtopic) and searching the result for the value. Values are tested using [`test_values`](#qlobberprototypetest_valuesvals-val).
**Parameters:**
- `{String} topic` The topic to match against.
- `{Any} val` The value being tested for.
**Return:**
`{Boolean}` Whether matching against `topic` contains `val`.
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.test_values(vals, val)
> Test whether values found in a match contain a value passed to [`test`](#qlobberprototypetesttopic-val). You can override this to provide a custom implementation. Defaults to using `indexOf`.
**Parameters:**
- `{Array} vals` The values found while matching.
- `{Any} val` The value being tested for.
**Return:**
`{Boolean}` Whether `vals` contains `val`.
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.clear()
> Reset the qlobber.
Removes all topic matchers from the qlobber.
**Return:**
`{Qlobber}` The qlobber (for chaining).
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.visit()
> Visit each node in the qlobber's trie in turn.
**Return:**
`{Iterator}` An iterator on the trie. The iterator returns objects which, if fed (in the same order) to the function returned by [`get_restorer`](#qlobberprototypeget_restoreroptions) on a different qlobber, will build that qlobber's trie to the same state. The objects can be serialized using `JSON.stringify`, _if_ the values you store in the qlobber are also serializable.
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## Qlobber.prototype.get_restorer([options])
> Get a function which can restore the qlobber's trie to a state you retrieved
by calling [`visit`](#qlobberprototypevisit) on this or another qlobber.
**Parameters:**
- `{Object} [options]` Options for restoring the trie.
- `{Boolean} cache_adds` Whether to cache topics when rebuilding the trie. This only applies if you also passed `cache_adds` as true in the [constructor](#qlobberoptions).
**Return:**
`{Function}` Function to call in order to rebuild the qlobber's trie. You should call this repeatedly with the objects you received from a call to [`visit`](#qlobberprototypevisit). If you serialized the objects, remember to deserialize them first (e.g. with `JSON.parse`)!
<sub>Go: [TOC](#tableofcontents) | [Qlobber.prototype](#toc_qlobberprototype)</sub>
## QlobberDedup([options])
> Creates a new de-duplicating qlobber.
Inherits from [`Qlobber`](#qlobberoptions).
**Parameters:**
- `{Object} [options]` Same options as [Qlobber](#qlobberoptions).
<sub>Go: [TOC](#tableofcontents)</sub>
<a name="qlobberdedupprototype"></a>
## QlobberDedup.prototype.test_values(vals, val)
> Test whether values found in a match contain a value passed to [`test`](#qlobberprototypetesttopic_val). You can override this to provide a custom implementation. Defaults to using `has`.
**Parameters:**
- `{Set} vals` The values found while matching ([ES6 Set](http://www.ecma-international.org/ecma-262/6.0/#sec-set-objects)).
- `{Any} val` The value being tested for.
**Return:**
`{Boolean}` Whether `vals` contains `val`.
<sub>Go: [TOC](#tableofcontents) | [QlobberDedup.prototype](#toc_qlobberdedupprototype)</sub>
## QlobberDedup.prototype.match(topic)
> Match a topic.
**Parameters:**
- `{String} topic` The topic to match against.
**Return:**
`{Set}` [ES6 Set](http://www.ecma-international.org/ecma-262/6.0/#sec-set-objects) of values that matched the topic.
<sub>Go: [TOC](#tableofcontents) | [QlobberDedup.prototype](#toc_qlobberdedupprototype)</sub>
## QlobberTrue([options])
> Creates a new qlobber which only stores the value `true`.
Whatever value you [`add`](#qlobberprototypeaddtopic-val) to this qlobber
(even `undefined`), a single, de-duplicated `true` will be stored. Use this
qlobber if you only need to test whether topics match, not about the values
they match to.
Inherits from [`Qlobber`](#qlobberoptions).
**Parameters:**
- `{Object} [options]` Same options as [Qlobber](#qlobberoptions).
<sub>Go: [TOC](#tableofcontents)</sub>
<a name="qlobbertrueprototype"></a>
## QlobberTrue.prototype.test_values()
> This override of [`test_values`](#qlobberprototypetest_valuesvals-val) always
returns `true`. When you call [`test`](#qlobberprototypetesttopic-val) on a
`QlobberTrue` instance, the value you pass is ignored since it only cares
whether a topic is matched.
**Return:**
`{Boolean}` Always `true`.
<sub>Go: [TOC](#tableofcontents) | [QlobberTrue.prototype](#toc_qlobbertrueprototype)</sub>
## QlobberTrue.prototype.match(topic)
> Match a topic.
Since `QlobberTrue` only cares whether a topic is matched and not about values
it matches to, this override of [`match`](#qlobberprototypematchtopic) just
calls [`test`](#qlobberprototypetesttopic-val) (with value `undefined`).
**Parameters:**
- `{String} topic` The topic to match against.
**Return:**
`{Boolean}` Whether the `QlobberTrue` instance matches the topic.
<sub>Go: [TOC](#tableofcontents) | [QlobberTrue.prototype](#toc_qlobbertrueprototype)</sub>
## VisitorStream(qlobber)
> Creates a new [`Readable`](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_class_stream_readable) stream, in object mode, which calls [`visit`](#qlobberprototypevisit) on a qlobber to generate its data.
You could [`pipe`](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_pipe_destination_options) this to a [`JSONStream.stringify`](https://github.com/dominictarr/JSONStream#jsonstreamstringifyopen-sep-close) stream, for instance, to serialize the qlobber to JSON. See [this test](test/json.js#L14) for an example.
Inherits from [`Readable`](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_class_stream_readable).
**Parameters:**
- `{Qlobber} qlobber` The qlobber to call [`visit`](#qlobberprototypevisit) on.
<sub>Go: [TOC](#tableofcontents)</sub>
## RestorerStream(qlobber)
> Creates a new [`Writable`](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_class_stream_writable) stream, in object mode, which passes data written to it into the function returned by calling [`get_restorer`](#qlobberprototypeget_restoreroptions) on a qlobber.
You could [`pipe`](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_pipe_destination_options) a [`JSONStream.parse`](https://github.com/dominictarr/JSONStream#jsonstreamparsepath) stream to this, for instance, to deserialize the qlobber from JSON. See [this test](test/json.js#L33) for an example.
Inherits from [`Writable`](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_class_stream_writable).
**Parameters:**
- `{Qlobber} qlobber` The qlobber to call [`get_restorer`](#qlobberprototypeget_restoreroptions) on.
<sub>Go: [TOC](#tableofcontents)</sub>
## set_native(qlobber_native)
> Add [qlobber-native](https://www.npmjs.com/package/qlobber-native) to qlobber.
**Parameters:**
- `{Object} qlobber_native` The qlobber-native module, obtained using `require('qlobber-native')`.
**Return:**
`{Object}` The qlobber exports with the following native classes added:
- `Qlobber.nativeString`
- `Qlobber.nativeNumber`
- `QlobberDedup.nativeString`
- `QlobberDedup.nativeNumber`
- `QlobberTrue.native`
<sub>Go: [TOC](#tableofcontents)</sub>
_&mdash;generated by [apidox](https://github.com/codeactual/apidox)&mdash;_
@@ -0,0 +1,122 @@
/*jslint node: true */
"use strict";
var util = require('util'),
qlobber = require('..'),
Qlobber = qlobber.Qlobber;
function QlobberSub (options)
{
Qlobber.call(this, options);
this.subscriptionsCount = 0;
}
util.inherits(QlobberSub, Qlobber);
QlobberSub.prototype._initial_value = function (val)
{
this.subscriptionsCount += 1;
let r = {
topic: val.topic,
clientMap: new Map().set(val.clientId, { qos: val.qos, rh: val.rh, rap: val.rap, nl: val.nl }),
};
r[Symbol.iterator] = function* (topic)
{
if (topic === undefined)
{
for (let [clientId, sub] of r.clientMap)
{
yield { topic: r.topic, clientId, ...sub };
}
}
else if (r.topic === topic)
{
for (let [clientId, sub] of r.clientMap)
{
yield { clientId, ...sub };
}
}
};
return r;
};
QlobberSub.prototype._add_value = function (existing, val)
{
var clientMap = existing.clientMap,
size = clientMap.size;
clientMap.set(val.clientId, { qos: val.qos, rh: val.rh, rap: val.rap, nl: val.nl });
if (clientMap.size > size)
{
this.subscriptionsCount += 1;
}
};
QlobberSub.prototype._add_values = function (dest, existing, topic)
{
if (topic === undefined)
{
for (let [clientId, sub] of existing.clientMap)
{
dest.push({ clientId, topic: existing.topic, ...sub });
}
}
else if (existing.topic === topic)
{
for (let [clientId, sub] of existing.clientMap)
{
dest.push({ clientId,...sub });
}
}
};
QlobberSub.prototype._remove_value = function (existing, val)
{
var clientMap = existing.clientMap,
size_before = clientMap.size;
clientMap.delete(val.clientId);
var size_after = clientMap.size;
if (size_after < size_before)
{
this.subscriptionsCount -= 1;
}
return size_after === 0;
};
// Returns whether client is last subscriber to topic
QlobberSub.prototype.test_values = function (existing, val)
{
var clientMap = existing.clientMap;
return (existing.topic === val.topic) &&
(clientMap.size === 1) &&
clientMap.has(val.clientId);
};
QlobberSub.prototype.match = function (topic, ctx)
{
return this._match([], 0, topic.split(this._separator), this._trie, ctx);
};
QlobberSub.prototype.clear = function ()
{
this.subscriptionsCount = 0;
return Qlobber.prototype.clear.call(this);
};
QlobberSub.set_native = function (qlobber_native)
{
const wrap_native = require('../lib/wrap_native.js');
QlobberSub.native = wrap_native(qlobber_native.QlobberSub, QlobberSub);
return module.exports;
};
module.exports = QlobberSub;
+3
View File
@@ -0,0 +1,3 @@
/*jslint node: true*/
"use strict";
module.exports = require('./lib/qlobber');
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
"use strict";
const util = require('util');
module.exports = function(QlobberNative, Qlobber)
{
class WrappedQlobberNative extends QlobberNative
{
constructor(options) {
super(options);
this.addP = util.promisify(this.add_async);
this.removeP = util.promisify(this.remove_async);
this.matchP = util.promisify(this.match_async);
this.testP = util.promisify(this.test_async);
this.clearP = util.promisify(this.clear_async);
this._get_visitorP = util.promisify(this.get_visitor_async);
this._visit_nextP = util.promisify(this.visit_next_async);
this._get_restorerP = util.promisify(this.get_restorer_async);
this._restore_nextP = util.promisify(this.restore_next_async);
this._match_iterP = util.promisify(this.match_iter_async);
this._match_nextP = util.promisify(this.match_next_async);
}
*visit()
{
const visitor = this.get_visitor();
while (true)
{
const v = this.visit_next(visitor);
if (v === undefined)
{
break;
}
yield v;
}
}
async *visitP()
{
const visitor = await this._get_visitorP();
while (true)
{
const v = await this._visit_nextP(visitor);
if (v === undefined)
{
break;
}
yield v;
}
}
*match_iter(topic, ctx) {
const iterator = super.match_iter(topic, ctx);
while (true)
{
const v = this.match_next(iterator);
if (v === undefined)
{
break;
}
yield v.value;
}
}
async *match_iterP(topic, ctx) {
const iterator = await this._match_iterP(topic, ctx);
while (true)
{
const v = await this._match_nextP(iterator);
if (v === undefined)
{
break;
}
yield v.value;
}
}
get_trie()
{
const qlobber = new Qlobber(this.options);
const restorer = qlobber.get_restorer();
for (let v of this.visit())
{
restorer(v);
}
return qlobber.get_trie();
}
get_restorer(options)
{
const restorer = super.get_restorer(options);
return obj => {
super.restore_next(restorer, obj);
};
}
async get_restorerP(options)
{
const restorer = await this._get_restorerP(options);
return async obj => {
await this._restore_nextP(restorer, obj);
};
}
}
WrappedQlobberNative.is_native = true;
WrappedQlobberNative.nonNative = Qlobber;
return WrappedQlobberNative;
};
+54
View File
@@ -0,0 +1,54 @@
{
"name": "qlobber",
"description": "Node.js globbing for amqp-like topics",
"version": "8.0.1",
"homepage": "https://github.com/davedoesdev/qlobber",
"author": {
"name": "David Halls",
"email": "dave@davedoesdev.com",
"url": "http://www.davedoesdev.com"
},
"repository": {
"type": "git",
"url": "https://github.com/davedoesdev/qlobber.git"
},
"bugs": {
"url": "https://github.com/davedoesdev/qlobber/issues"
},
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "(cd native && npm install && node-gyp rebuild --debug) && grunt test",
"coverage": "(cd native && npm install && node-gyp rebuild --debug) && grunt coverage"
},
"directories": {
"test": "test",
"lib": "lib"
},
"keywords": [
"amqp",
"mqtt",
"rabbitmq",
"ascoltatore"
],
"engines": {
"node": ">= 16"
},
"devDependencies": {
"@davedoesdev/b": "^5.0.3",
"c8": "^9.1.0",
"chai": "^5.0.3",
"grunt": "^1.6.1",
"grunt-apidox": "^3.1.1",
"grunt-contrib-jshint": "^3.2.0",
"grunt-exec": "^3.0.0",
"JSONStream": "^1.3.5",
"mocha": "^10.2.0",
"stream-buffers": "^3.0.2"
},
"overrides": {
"apidox": {
"dox": "^1.0.0"
}
}
}
+63
View File
@@ -0,0 +1,63 @@
{
"name": "mqemitter",
"version": "6.0.2",
"description": "An Opinionated Message Queue with an emitter-style API",
"main": "mqemitter.js",
"types": "types/mqemitter.d.ts",
"scripts": {
"lint": "npm run lint:standard && npm run lint:typescript && npm run lint:markdown",
"lint:standard": "standard --verbose | snazzy",
"lint:typescript": "standard --parser @typescript-eslint/parser --plugin @typescript-eslint/eslint-plugin test/types/*.ts types/mqemitter.d.ts",
"lint:markdown": "markdownlint README.md",
"unit": "tape test/*.js",
"unit:report": "nyc --reporter=html --reporter=cobertura --reporter=text tape test/*.js",
"unit:cov": "nyc --reporter=text tape test/*.js",
"typescript": "tsc --project ./test/types/tsconfig.json",
"test:report": "npm run lint && npm run unit:report && npm run typescript",
"test:types": "tsd",
"test": "npm run lint && npm run unit:cov && tsd && npm run typescript"
},
"pre-commit": [
"test"
],
"website": "https://github.com/mcollina/mqemitter",
"repository": {
"type": "git",
"url": "https://github.com/mcollina/mqemitter.git"
},
"bugs": {
"url": "http://github.com/mcollina/mqemitter/issues"
},
"author": "Matteo Collina <hello@matteocollina.com>",
"engines": {
"node": ">=16"
},
"keywords": [
"emitter",
"events",
"message queue",
"mq",
"publish",
"subscribe",
"pub",
"sub"
],
"license": "ISC",
"devDependencies": {
"@types/node": "^16.11.1",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"markdownlint-cli": "^0.31.1",
"nyc": "^15.0.0",
"pre-commit": "^1.2.2",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"tape": "^5.0.1",
"tsd": "^0.31.0",
"typescript": "^5.4.5"
},
"dependencies": {
"fastparallel": "^2.4.1",
"qlobber": "^8.0.1"
}
}
+180
View File
@@ -0,0 +1,180 @@
'use strict'
const { test } = require('tape')
const mq = require('../')
require('../abstractTest')({
builder: mq,
test
})
test('queue concurrency', function (t) {
t.plan(3)
const e = mq({ concurrency: 1 })
let completed1 = false
t.equal(e.concurrency, 1)
e.on('hello 1', function (message, cb) {
setTimeout(cb, 10)
})
e.on('hello 2', function (message, cb) {
cb()
})
e.emit({ topic: 'hello 1' }, function () {
completed1 = true
})
e.emit({ topic: 'hello 2' }, function () {
t.ok(completed1, 'the first message must be completed')
})
t.equal(e.length, 1)
})
test('queue released when full', function (t) {
t.plan(21)
const e = mq({ concurrency: 1 })
e.on('hello 1', function (message, cb) {
t.ok(true, 'message received')
setTimeout(cb, 10)
})
function onSent () {
t.ok(true, 'message sent')
}
for (let i = 0; i < 9; i++) {
e._messageQueue.push({ topic: 'hello 1' })
e._messageCallbacks.push(onSent)
e.current++
}
e.emit({ topic: 'hello 1' }, onSent)
process.once('warning', function (warning) {
t.equal(warning.message, 'MqEmitter leak detected', 'warning message')
})
})
test('without any listeners and a callback', function (t) {
const e = mq()
const expected = {
topic: 'hello world',
payload: { my: 'message' }
}
e.emit(expected, function () {
t.equal(e.current, 1, 'there 1 message that is being processed')
e.close(function () {
t.end()
})
})
})
test('queue concurrency with overlapping subscriptions', function (t) {
t.plan(3)
const e = mq({ concurrency: 1 })
let completed1 = false
t.equal(e.concurrency, 1)
e.on('000001/021/#', function (message, cb) {
setTimeout(cb, 10)
})
e.on('000001/021/000B/0001/01', function (message, cb) {
setTimeout(cb, 20)
})
e.emit({ topic: '000001/021/000B/0001/01' }, function () {
completed1 = true
})
e.emit({ topic: '000001/021/000B/0001/01' }, function () {
t.ok(completed1, 'the first message must be completed')
process.nextTick(function () {
t.equal(e.current, 0, 'no message is in flight')
})
})
})
test('removeListener without a callback does not throw', function (t) {
const e = mq()
function fn () {}
e.on('hello', fn)
e.removeListener('hello', fn)
t.end()
})
test('removeAllListeners removes listeners', function (t) {
const e = mq()
e.on('hello', function () {
t.fail('listener called')
})
e.removeAllListeners('hello', function () {
e.emit({ topic: 'hello' }, function () {
t.end()
})
})
})
test('removeAllListeners without a callback does not throw', function (t) {
const e = mq()
function fn () {}
e.on('hello', fn)
e.removeAllListeners('hello')
t.end()
})
test('set defaults to opts', function (t) {
const opts = {}
mq(opts)
t.deepEqual(opts, {
matchEmptyLevels: true,
separator: '/',
wildcardOne: '+',
wildcardSome: '#'
})
t.end()
})
test('removeListener inside messageHandler', function (t) {
t.plan(3)
const e = mq()
function messageHandler1 (message, cb) {
t.ok(true, 'messageHandler1 called')
// removes itself
e.removeListener('hello', messageHandler1)
cb()
}
e.on('hello', messageHandler1)
function messageHandler2 (message, cb) {
t.ok(true, 'messageHandler2 called')
cb()
}
e.on('hello', messageHandler2)
e.emit({ topic: 'hello' }, function () {
t.ok(true, 'emit callback received')
})
})
+46
View File
@@ -0,0 +1,46 @@
/* eslint no-unused-vars: 0 */
/* eslint no-undef: 0 */
import MQEmitter, { Message } from '../../types/mqemitter'
const noop = function () {}
let mq = MQEmitter()
mq = MQEmitter({
concurrency: 100
})
mq.close(noop)
mq = MQEmitter({
concurrency: 100,
matchEmptyLevels: true,
separator: ','
})
mq.close(noop)
mq = MQEmitter({
concurrency: 10,
matchEmptyLevels: true,
separator: '/',
wildcardOne: '+',
wildcardSome: '#'
})
const notify = function (msg: Message, cb: () => void) {
if (msg.topic === 'hello/world') {
console.log(msg)
}
cb()
}
mq.on('hello/+', notify)
mq.emit({ topic: 'hello/world', payload: 'or any other fields', [Symbol.for('me')]: 42 })
mq.emit({ topic: 'hello/world' }, function (err) {
console.log(err)
})
mq.removeListener('hello/+', notify)
mq.close(noop)
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noEmit": true,
"strict": true
},
"files": [
"./index.ts"
]
}
+22
View File
@@ -0,0 +1,22 @@
/// <reference types="node" />
interface MQEmitterOptions {
concurrency?: number
matchEmptyLevels?: boolean
separator?: string
wildcardOne?: string
wildcardSome?: string
}
export type Message = Record<string, any> & { topic: string }
export interface MQEmitter {
current: number
concurrent: number
on(topic: string, listener: (message: Message, done: () => void) => void, callback?: () => void): this
emit(message: Message, callback?: (error?: Error) => void): void
removeListener(topic: string, listener: (message: Message, done: () => void) => void, callback?: () => void): void
close(callback: () => void): void
}
export default function (options?: MQEmitterOptions): MQEmitter
+32
View File
@@ -0,0 +1,32 @@
import { expectError, expectType } from 'tsd';
import mqEmitter, { Message, MQEmitter } from './mqemitter';
expectType<MQEmitter>(mqEmitter());
expectType<MQEmitter>(mqEmitter({ concurrency: 200, matchEmptyLevels: true }));
expectType<MQEmitter>(
mqEmitter({
concurrency: 10,
matchEmptyLevels: true,
separator: '/',
wildcardOne: '+',
wildcardSome: '#',
})
);
function listener(message: Message, done: () => void) {}
expectType<MQEmitter>(mqEmitter().on('topic', listener));
expectError(mqEmitter().emit(null));
expectType<void>(
mqEmitter().emit({ topic: 'test', prop1: 'prop1', [Symbol.for('me')]: 42 })
);
expectType<void>(mqEmitter().emit({ topic: 'test', prop1: 'prop1' }, () => {}));
expectType<void>(mqEmitter().removeListener('topic', listener));
expectType<void>(mqEmitter().close(() => null));