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']); + }); + }); +});