UNPKG

rocket.chat.mqtt

Version:

It's a MQTT Server, using redis to scale horizontally.

641 lines (510 loc) 15.2 kB
/** # qlobber&nbsp;&nbsp;&nbsp;[![Build Status](https://travis-ci.org/davedoesdev/qlobber.png)](https://travis-ci.org/davedoesdev/qlobber) [![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. 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;