1127 lines
30 KiB
JavaScript
1127 lines
30 KiB
JavaScript
/**
|
|
# qlobber [](https://github.com/davedoesdev/qlobber/actions/workflows/ci.yml) [](https://coveralls.io/r/davedoesdev/qlobber?branch=master) [](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
|
|
*/
|
|
|
|
/*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 _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.
|
|
*/
|
|
function Qlobber (options)
|
|
{
|
|
options = options || {};
|
|
|
|
this._separator = options.separator || '.';
|
|
this._wildcard_one = options.wildcard_one || '*';
|
|
this._wildcard_some = options.wildcard_some || '#';
|
|
this._max_words = options.max_words || 100;
|
|
this._max_wildcard_somes = options.max_wildcard_somes || 3;
|
|
this._match_empty_levels = options.match_empty_levels;
|
|
this._trie = new Map();
|
|
if (options.cache_adds instanceof Map)
|
|
{
|
|
this._shortcuts = options.cache_adds;
|
|
}
|
|
else if (options.cache_adds)
|
|
{
|
|
this._shortcuts = new Map();
|
|
}
|
|
if (options.cache_splits > 0)
|
|
{
|
|
this._cache_splits = options.cache_splits;
|
|
this._split_cache = 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._iter_values = function (origin, ctx)
|
|
{
|
|
return origin[Symbol.iterator](ctx);
|
|
};
|
|
|
|
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 || this._match_empty_levels)
|
|
{
|
|
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, this._split(topic, false), 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 || this._match_empty_levels)
|
|
{
|
|
st = sub_trie.get(this._wildcard_one);
|
|
|
|
if (st && this._test(v, i + 1, words, st))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
Qlobber.prototype._match_some_iter = function* (i, words, st, ctx)
|
|
{
|
|
var j, w;
|
|
|
|
for (w of st.keys())
|
|
{
|
|
if (w !== this._separator)
|
|
{
|
|
for (j = i; j < words.length; j += 1)
|
|
{
|
|
yield* this._match_iter(j, words, st, ctx);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
Qlobber.prototype._match_iter = function* (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...
|
|
yield* this._match_some_iter(i, words, st, ctx);
|
|
// and we'll end up matching the rest of the words:
|
|
yield* this._match_iter(words.length, words, st, ctx);
|
|
}
|
|
|
|
if (i === words.length)
|
|
{
|
|
st = sub_trie.get(this._separator);
|
|
|
|
if (st)
|
|
{
|
|
yield* this._iter_values(st, ctx);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
word = words[i];
|
|
|
|
if ((word !== this._wildcard_one) && (word !== this._wildcard_some))
|
|
{
|
|
st = sub_trie.get(word);
|
|
|
|
if (st)
|
|
{
|
|
yield* this._match_iter(i + 1, words, st, ctx);
|
|
}
|
|
}
|
|
|
|
if (word || this._match_empty_levels)
|
|
{
|
|
st = sub_trie.get(this._wildcard_one);
|
|
|
|
if (st)
|
|
{
|
|
yield* this._match_iter(i + 1, words, st, ctx);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Qlobber.prototype._split_words = function (topic) {
|
|
const r = topic.split(this._separator);
|
|
if (r.length > this._max_words) {
|
|
throw new Error('too many words');
|
|
}
|
|
return r;
|
|
};
|
|
|
|
Qlobber.prototype._split = function (topic, adding) {
|
|
let r;
|
|
if (this._split_cache) {
|
|
r = this._split_cache.get(topic);
|
|
if (r === undefined) {
|
|
r = this._split_words(topic);
|
|
this._split_cache.set(topic, r);
|
|
if (this._split_cache.size > this._cache_splits) {
|
|
for (let t of this._split_cache.keys()) {
|
|
this._split_cache.delete(t);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
r = this._split_words(topic);
|
|
}
|
|
if (adding &&
|
|
(r.reduce((n, w) => n + (w === this._wildcard_some), 0) > this._max_wildcard_somes)) {
|
|
throw new Error('too many wildcard somes');
|
|
}
|
|
return r;
|
|
};
|
|
|
|
/**
|
|
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, this._split(topic, true), 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, this._split(topic, false), 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. Use a [`QlobberDedup`](#qlobberdedupoptions) if you don't want duplicates.
|
|
*/
|
|
Qlobber.prototype.match = function (topic, ctx)
|
|
{
|
|
return this._match2([], topic, ctx);
|
|
};
|
|
|
|
/**
|
|
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).
|
|
*/
|
|
Qlobber.prototype.match_iter = function (topic, ctx)
|
|
{
|
|
return this._match_iter(0, this._split(topic, false), this._trie, 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, this._split(topic, false), 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();
|
|
}
|
|
if (this._split_cache)
|
|
{
|
|
this._split_cache.clear();
|
|
}
|
|
return this;
|
|
};
|
|
|
|
// for debugging
|
|
Qlobber.prototype.get_trie = function ()
|
|
{
|
|
return this._trie;
|
|
};
|
|
|
|
/**
|
|
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.
|
|
*/
|
|
Qlobber.prototype.visit = function* ()
|
|
{
|
|
let iterators = [],
|
|
iterator = this._trie.entries(),
|
|
i = 0;
|
|
|
|
while (true)
|
|
{
|
|
if (i === 0)
|
|
{
|
|
yield { type: 'start_entries' };
|
|
}
|
|
|
|
let next = iterator.next();
|
|
|
|
if (next.done)
|
|
{
|
|
yield { type: 'end_entries' };
|
|
|
|
let prev = iterators.pop();
|
|
if (prev === undefined)
|
|
{
|
|
return;
|
|
}
|
|
|
|
[iterator, i] = prev;
|
|
continue;
|
|
}
|
|
|
|
let [key, value] = next.value;
|
|
yield { type: 'entry', key: key };
|
|
++i;
|
|
|
|
if (key === this._separator)
|
|
{
|
|
yield { type: 'start_values' };
|
|
|
|
if (value[Symbol.iterator])
|
|
{
|
|
for (let v of value)
|
|
{
|
|
yield { type: 'value', value: v };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
yield { type: 'value', value: value };
|
|
}
|
|
|
|
yield { type: 'end_values' };
|
|
continue;
|
|
}
|
|
|
|
iterators.push([iterator, i]);
|
|
iterator = value.entries();
|
|
i = 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
Get a function which can restore the qlobber's trie to a state you retrieved
|
|
by calling [`visit`](#qlobberprototypevisit) on this or another qlobber.
|
|
|
|
@param {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`)!
|
|
*/
|
|
Qlobber.prototype.get_restorer = function (options)
|
|
{
|
|
options = options || {};
|
|
|
|
let sts = [],
|
|
entry = this._trie,
|
|
path = '';
|
|
|
|
return obj =>
|
|
{
|
|
switch (obj.type)
|
|
{
|
|
case 'entry':
|
|
entry = entry || new Map();
|
|
sts.push([entry, obj.key, path]);
|
|
entry = entry.get(obj.key);
|
|
if (options.cache_adds)
|
|
{
|
|
if (path)
|
|
{
|
|
path += this._separator;
|
|
}
|
|
path += obj.key;
|
|
}
|
|
break;
|
|
|
|
case 'value':
|
|
if (entry)
|
|
{
|
|
this._add_value(entry, obj.value);
|
|
}
|
|
else
|
|
{
|
|
entry = this._initial_value(obj.value);
|
|
}
|
|
break;
|
|
|
|
case 'end_entries':
|
|
if (entry && (entry.size === 0))
|
|
{
|
|
entry = undefined;
|
|
}
|
|
/* falls through */
|
|
|
|
case 'end_values':
|
|
let prev = sts.pop();
|
|
if (prev === undefined)
|
|
{
|
|
entry = undefined;
|
|
path = '';
|
|
}
|
|
else
|
|
{
|
|
let [prev_entry, key, prev_path] = prev;
|
|
if (entry)
|
|
{
|
|
if (options.cache_adds &&
|
|
this._shortcuts &&
|
|
(obj.type === 'end_values'))
|
|
{
|
|
this._shortcuts.set(prev_path, entry);
|
|
}
|
|
prev_entry.set(key, entry);
|
|
}
|
|
entry = prev_entry;
|
|
path = prev_path;
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
Creates a new de-duplicating qlobber.
|
|
|
|
Inherits from [`Qlobber`](#qlobberoptions).
|
|
|
|
@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`](#qlobberoptions).
|
|
|
|
@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;
|
|
};
|
|
|
|
QlobberTrue.prototype._iter_values = function* ()
|
|
{
|
|
yield 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);
|
|
};
|
|
|
|
let stream = require('stream');
|
|
|
|
/**
|
|
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).
|
|
|
|
@constructor
|
|
|
|
@param {Qlobber} qlobber The qlobber to call [`visit`](#qlobberprototypevisit) on.
|
|
*/
|
|
function VisitorStream (qlobber)
|
|
{
|
|
stream.Readable.call(this, { objectMode: true });
|
|
this._iterator = qlobber.visit();
|
|
}
|
|
|
|
util.inherits(VisitorStream, stream.Readable);
|
|
|
|
VisitorStream.prototype._read = function ()
|
|
{
|
|
while (true)
|
|
{
|
|
let { done, value } = this._iterator.next();
|
|
|
|
if (done)
|
|
{
|
|
this.push(null);
|
|
break;
|
|
}
|
|
|
|
if (!this.push(value))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
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).
|
|
|
|
@constructor
|
|
|
|
@param {Qlobber} qlobber The qlobber to call [`get_restorer`](#qlobberprototypeget_restoreroptions) on.
|
|
*/
|
|
function RestorerStream (qlobber)
|
|
{
|
|
stream.Writable.call(this, { objectMode: true });
|
|
this._restorer = qlobber.get_restorer();
|
|
}
|
|
|
|
util.inherits(RestorerStream, stream.Writable);
|
|
|
|
RestorerStream.prototype._write = function (value, _, cb)
|
|
{
|
|
this._restorer(value);
|
|
cb();
|
|
};
|
|
|
|
let native_module = null;
|
|
|
|
/**
|
|
Add [qlobber-native](https://www.npmjs.com/package/qlobber-native) to qlobber.
|
|
|
|
@param {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`
|
|
*/
|
|
function set_native(qlobber_native)
|
|
{
|
|
if (qlobber_native !== native_module)
|
|
{
|
|
const wrap_native = require('./wrap_native.js');
|
|
Qlobber.nativeString = wrap_native(qlobber_native.QlobberString, Qlobber);
|
|
Qlobber.nativeNumber = wrap_native(qlobber_native.QlobberNumber, Qlobber);
|
|
QlobberDedup.nativeString = wrap_native(qlobber_native.QlobberDedupString, QlobberDedup);
|
|
QlobberDedup.nativeNumber = wrap_native(qlobber_native.QlobberDedupNumber, QlobberDedup);
|
|
QlobberTrue.native = wrap_native(qlobber_native.QlobberTrue, QlobberTrue);
|
|
native_module = qlobber_native;
|
|
}
|
|
|
|
return exports;
|
|
}
|
|
|
|
exports.Qlobber = Qlobber;
|
|
exports.QlobberDedup = QlobberDedup;
|
|
exports.QlobberTrue = QlobberTrue;
|
|
exports.VisitorStream = VisitorStream;
|
|
exports.RestorerStream = RestorerStream;
|
|
exports.set_native = set_native;
|