rocket.chat.mqtt
Version:
It's a MQTT Server, using redis to scale horizontally.
641 lines (510 loc) • 15.2 kB
JavaScript
/**
# qlobber [](https://travis-ci.org/davedoesdev/qlobber) [](https://coveralls.io/r/davedoesdev/qlobber?branch=master) [](http://badge.fury.io/js/qlobber)
Node.js globbing for amqp-like topics.
Example:
```javascript
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 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']]);
```
## 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
grunt test
```
## Lint
```shell
grunt lint
```
## Code Coverage
```shell
grunt coverage
```
[Instanbul](http://gotwarlost.github.io/istanbul/) 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).
# API
*/
/*jslint node: true, nomen: true */
"use strict";
var util = require('util');
/**
Creates a new qlobber.
@constructor
@param {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 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} 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`.
*/
function Qlobber (options)
{
options = options || {};
this._separator = options.separator || '.';
this._wildcard_one = options.wildcard_one || '*';
this._wildcard_some = options.wildcard_some || '#';
this._trie = new Map();
if (options.cache_adds)
{
this._shortcuts = new Map();
}
}
Qlobber.prototype._initial_value = function (val)
{
return [val];
};
Qlobber.prototype._add_value = function (vals, val)
{
vals[vals.length] = val;
};
Qlobber.prototype._add_values = function (dest, origin)
{
var i, destLength = dest.length, originLength = origin.length;
for (i = 0; i < originLength; i += 1)
{
dest[destLength + i] = origin[i];
}
};
Qlobber.prototype._remove_value = function (vals, val)
{
if (val === undefined)
{
return true;
}
var index = vals.lastIndexOf(val);
if (index >= 0)
{
vals.splice(index, 1);
}
return vals.length === 0;
};
Qlobber.prototype._add = function (val, i, words, sub_trie)
{
var st, word;
if (i === words.length)
{
st = sub_trie.get(this._separator);
if (st)
{
this._add_value(st, val);
}
else
{
st = this._initial_value(val);
sub_trie.set(this._separator, st);
}
return st;
}
word = words[i];
st = sub_trie.get(word);
if (!st)
{
st = new Map();
sub_trie.set(word, st);
}
return this._add(val, i + 1, words, st);
};
Qlobber.prototype._remove = function (val, i, words, sub_trie)
{
var st, word, r;
if (i === words.length)
{
st = sub_trie.get(this._separator);
if (st && this._remove_value(st, val))
{
sub_trie.delete(this._separator);
return true;
}
return false;
}
word = words[i];
st = sub_trie.get(word);
if (!st)
{
return false;
}
r = this._remove(val, i + 1, words, st);
if (st.size === 0)
{
sub_trie.delete(word);
}
return r;
};
Qlobber.prototype._match_some = function (v, i, words, st, ctx)
{
var j, w;
for (w of st.keys())
{
if (w !== this._separator)
{
for (j = i; j < words.length; j += 1)
{
v = this._match(v, j, words, st, ctx);
}
break;
}
}
return v;
};
Qlobber.prototype._match = function (v, i, words, sub_trie, ctx)
{
var word, st;
st = sub_trie.get(this._wildcard_some);
if (st)
{
// in the common case there will be no more levels...
v = this._match_some(v, i, words, st, ctx);
// and we'll end up matching the rest of the words:
v = this._match(v, words.length, words, st, ctx);
}
if (i === words.length)
{
st = sub_trie.get(this._separator);
if (st)
{
if (v.dest)
{
this._add_values(v.dest, v.source, ctx);
this._add_values(v.dest, st, ctx);
v = v.dest;
}
else if (v.source)
{
v.dest = v.source;
v.source = st;
}
else
{
this._add_values(v, st, ctx);
}
}
}
else
{
word = words[i];
if ((word !== this._wildcard_one) && (word !== this._wildcard_some))
{
st = sub_trie.get(word);
if (st)
{
v = this._match(v, i + 1, words, st, ctx);
}
}
if (word)
{
st = sub_trie.get(this._wildcard_one);
if (st)
{
v = this._match(v, i + 1, words, st, ctx);
}
}
}
return v;
};
Qlobber.prototype._match2 = function (v, topic, ctx)
{
var vals = this._match(
{
source: v
}, 0, topic.split(this._separator), this._trie, ctx);
return vals.source || vals;
};
Qlobber.prototype._test_some = function (v, i, words, st)
{
var j, w;
for (w of st.keys())
{
if (w !== this._separator)
{
for (j = i; j < words.length; j += 1)
{
if (this._test(v, j, words, st))
{
return true;
}
}
break;
}
}
return false;
};
Qlobber.prototype._test = function (v, i, words, sub_trie)
{
var word, st;
st = sub_trie.get(this._wildcard_some);
if (st)
{
// in the common case there will be no more levels...
if (this._test_some(v, i, words, st) ||
// and we'll end up matching the rest of the words:
this._test(v, words.length, words, st))
{
return true;
}
}
if (i === words.length)
{
st = sub_trie.get(this._separator);
if (st && this.test_values(st, v))
{
return true;
}
}
else
{
word = words[i];
if ((word !== this._wildcard_one) && (word !== this._wildcard_some))
{
st = sub_trie.get(word);
if (st && this._test(v, i + 1, words, st))
{
return true;
}
}
if (word)
{
st = sub_trie.get(this._wildcard_one);
if (st && this._test(v, i + 1, words, st))
{
return true;
}
}
}
return false;
};
/**
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.
@param {String} topic The topic to match against.
@param {Any} val The value to return if the topic is matched.
@return {Qlobber} The qlobber (for chaining).
*/
Qlobber.prototype.add = function (topic, val)
{
var shortcut = this._shortcuts && this._shortcuts.get(topic);
if (shortcut)
{
this._add_value(shortcut, val);
}
else
{
shortcut = this._add(val, 0, topic.split(this._separator), this._trie);
if (this._shortcuts)
{
this._shortcuts.set(topic, shortcut);
}
}
return this;
};
/**
Remove a topic matcher from the qlobber.
@param {String} topic The topic that's being matched against.
@param {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).
*/
Qlobber.prototype.remove = function (topic, val)
{
if (this._remove(val, 0, topic.split(this._separator), this._trie) && this._shortcuts)
{
this._shortcuts.delete(topic);
}
return this;
};
/**
Match a topic.
@param {String} topic The topic to match against.
@return {Array} List of values that matched the topic. This may contain duplicates.
*/
Qlobber.prototype.match = function (topic, ctx)
{
return this._match2([], topic, ctx);
};
/**
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).
@param {String} topic The topic to match against.
@param {Any} val The value being tested for.
@return {Boolean} Whether matching against `topic` contains `val`.
*/
Qlobber.prototype.test = function (topic, val)
{
return this._test(val, 0, topic.split(this._separator), this._trie);
};
/**
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`.
@param {Array} vals The values found while matching.
@param {Any} val The value being tested for.
@return {Boolean} Whether `vals` contains `val`.
*/
Qlobber.prototype.test_values = function (vals, val)
{
return vals.indexOf(val) >= 0;
};
/**
Reset the qlobber.
Removes all topic matchers from the qlobber.
@return {Qlobber} The qlobber (for chaining).
*/
Qlobber.prototype.clear = function ()
{
this._trie.clear();
if (this._shortcuts)
{
this._shortcuts.clear();
}
return this;
};
// for debugging
Qlobber.prototype.get_trie = function ()
{
return this._trie;
};
/**
Creates a new de-duplicating qlobber.
Inherits from Qlobber.
@constructor
@param {Object} [options] Same options as Qlobber.
*/
function QlobberDedup (options)
{
Qlobber.call(this, options);
}
util.inherits(QlobberDedup, Qlobber);
QlobberDedup.prototype._initial_value = function (val)
{
return new Set().add(val);
};
QlobberDedup.prototype._add_value = function (vals, val)
{
vals.add(val);
};
QlobberDedup.prototype._add_values = function (dest, origin)
{
origin.forEach(function (val)
{
dest.add(val);
});
};
QlobberDedup.prototype._remove_value = function (vals, val)
{
if (val === undefined)
{
return true;
}
vals.delete(val);
return vals.size === 0;
};
/**
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`.
@param {Set} vals The values found while matching ([ES6 Set](http://www.ecma-international.org/ecma-262/6.0/#sec-set-objects)).
@param {Any} val The value being tested for.
@return {Boolean} Whether `vals` contains `val`.
*/
QlobberDedup.prototype.test_values = function (vals, val)
{
return vals.has(val);
};
/**
Match a topic.
@param {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.
*/
QlobberDedup.prototype.match = function (topic, ctx)
{
return this._match2(new Set(), topic, ctx);
};
/**
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.
@constructor
@param {Object} [options] Same options as Qlobber.
*/
function QlobberTrue (options)
{
Qlobber.call(this, options);
}
util.inherits(QlobberTrue, Qlobber);
QlobberTrue.prototype._initial_value = function ()
{
return true;
};
QlobberTrue.prototype._add_value = function ()
{
};
QlobberTrue.prototype._remove_value = function ()
{
return true;
};
/**
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`.
*/
QlobberTrue.prototype.test_values = function ()
{
return true;
};
/**
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`).
@param {String} topic The topic to match against.
@return {Boolean} Whether the `QlobberTrue` instance matches the topic.
*/
QlobberTrue.prototype.match = function (topic, ctx)
{
return this.test(topic, ctx);
};
exports.Qlobber = Qlobber;
exports.QlobberDedup = QlobberDedup;
exports.QlobberTrue = QlobberTrue;