diff --git a/Gruntfile.js b/Gruntfile.js
index 6db32d085..9ca02afd2 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -100,7 +100,10 @@ module.exports = grunt => {
dest: 'js/libtextsecure.js',
},
libloki: {
- src: ['libloki/libloki-protocol.js'],
+ src: [
+ 'libloki/libloki-protocol.js',
+ 'libloki/service_nodes.js',
+ ],
dest: 'js/libloki.js',
},
lokitest: {
diff --git a/libloki/service_nodes.js b/libloki/service_nodes.js
new file mode 100644
index 000000000..21e676493
--- /dev/null
+++ b/libloki/service_nodes.js
@@ -0,0 +1,35 @@
+/* global window */
+
+// eslint-disable-next-line func-names
+(function () {
+ window.libloki = window.libloki || {};
+ window.libloki.serviceNodes = window.libloki.serviceNodes || {};
+
+ function consolidateLists(lists, threshold = 1){
+ if (typeof threshold !== 'number') {
+ throw Error('Provided threshold is not a number');
+ }
+
+ // calculate list size manually since `Set`
+ // does not have a `length` attribute
+ let numLists = 0;
+ const occurences = {};
+ lists.forEach(list => {
+ numLists += 1;
+ list.forEach(item => {
+ if (!(item in occurences)) {
+ occurences[item] = 1;
+ } else {
+ occurences[item] += 1;
+ }
+ });
+ });
+
+ const scaledThreshold = numLists * threshold;
+ return Object.entries(occurences)
+ .filter(keyValue => keyValue[1] >= scaledThreshold)
+ .map(keyValue => keyValue[0]);
+ }
+
+ window.libloki.serviceNodes.consolidateLists = consolidateLists;
+})();
diff --git a/libloki/test/index.html b/libloki/test/index.html
index 83a5b8319..9bd8fde01 100644
--- a/libloki/test/index.html
+++ b/libloki/test/index.html
@@ -25,9 +25,11 @@
+
+
diff --git a/libloki/test/service_nodes_test.js b/libloki/test/service_nodes_test.js
new file mode 100644
index 000000000..e502e8532
--- /dev/null
+++ b/libloki/test/service_nodes_test.js
@@ -0,0 +1,57 @@
+/* global libloki, chai */
+
+describe('ServiceNodes', () => {
+ describe('#consolidateLists', () => {
+ it('should throw when provided a non-iterable list', () => {
+ chai.expect(() => libloki.serviceNodes.consolidateLists(null, 1)).to.throw();
+ });
+ it('should throw when provided a non-iterable item in the list', () => {
+ chai.expect(() => libloki.serviceNodes.consolidateLists([1, 2, 3], 1)).to.throw();
+ });
+ it('should throw when provided a non-number threshold', () => {
+ chai.expect(() => libloki.serviceNodes.consolidateLists([], 'a')).to.throw();
+ });
+ it('should return an empty array when the input is an empty array', () => {
+ const result = libloki.serviceNodes.consolidateLists([]);
+ chai.expect(result).to.deep.equal([]);
+ });
+ it('should return the input when only 1 list is provided', () => {
+ const result = libloki.serviceNodes.consolidateLists([['a', 'b', 'c']]);
+ chai.expect(result).to.deep.equal(['a', 'b', 'c']);
+ });
+ it('should return the union of all lists when threshold is 0', () => {
+ const result = libloki.serviceNodes.consolidateLists([
+ ['a', 'b', 'c', 'h'],
+ ['d', 'e', 'f', 'g'],
+ ['g', 'h'],
+ ], 0);
+ chai.expect(result.sort()).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
+ });
+ it('should return the intersection of all lists when threshold is 1', () => {
+ const result = libloki.serviceNodes.consolidateLists([
+ ['a', 'b', 'c', 'd'],
+ ['a', 'e', 'f', 'g'],
+ ['a', 'h'],
+ ], 1);
+ chai.expect(result).to.deep.equal(['a']);
+ });
+ it('should return the elements that have an occurence >= the provided threshold', () => {
+ const result = libloki.serviceNodes.consolidateLists([
+ ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
+ ['a', 'b', 'c', 'd', 'e', 'f', 'h'],
+ ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
+ ['a', 'b', 'c', 'd', 'e', 'g', 'h'],
+ ], 3/4);
+ chai.expect(result).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
+ });
+ it('should work with sets as well', () => {
+ const result = libloki.serviceNodes.consolidateLists(new Set([
+ new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
+ new Set(['a', 'b', 'c', 'd', 'e', 'f', 'h']),
+ new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
+ new Set(['a', 'b', 'c', 'd', 'e', 'g', 'h']),
+ ]), 3/4);
+ chai.expect(result).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
+ });
+ });
+});