mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
add custom linkedlist-impl
This commit is contained in:
parent
f904781d61
commit
debdfee9c9
132
packages/unleash-api/lib/client-metrics/list.js
Normal file
132
packages/unleash-api/lib/client-metrics/list.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
constructor (value) {
|
||||||
|
this.value = value;
|
||||||
|
this.next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
link (next) {
|
||||||
|
this.next = next;
|
||||||
|
next.prev = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* linked list
|
||||||
|
* ranged list, assumes start to end(tail) is a known order
|
||||||
|
* remove() is only implemented in reverse order for the usecase
|
||||||
|
* emits events on eviction
|
||||||
|
*/
|
||||||
|
module.exports = class List extends EventEmitter {
|
||||||
|
constructor () {
|
||||||
|
super();
|
||||||
|
this.start = null;
|
||||||
|
this.tail = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
add (obj) {
|
||||||
|
const node = new Node(obj);
|
||||||
|
if (this.tail) {
|
||||||
|
this.tail.link(node);
|
||||||
|
this.tail = node;
|
||||||
|
} else {
|
||||||
|
this.start = node;
|
||||||
|
this.tail = node;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
iterate (fn) {
|
||||||
|
if (!this.start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cursor = this.start;
|
||||||
|
while (cursor) {
|
||||||
|
const result = fn(cursor);
|
||||||
|
if (result === false) {
|
||||||
|
cursor = null;
|
||||||
|
} else {
|
||||||
|
cursor = cursor.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iterateReverse (fn) {
|
||||||
|
if (!this.tail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cursor = this.tail;
|
||||||
|
while (cursor) {
|
||||||
|
const result = fn(cursor);
|
||||||
|
if (result === false) {
|
||||||
|
cursor = null;
|
||||||
|
} else {
|
||||||
|
cursor = cursor.prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reverseRemoveUntilTrue (fn) {
|
||||||
|
if (!this.tail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor = this.tail;
|
||||||
|
while (cursor) {
|
||||||
|
const result = fn(cursor);
|
||||||
|
if (result === false && cursor === this.start) {
|
||||||
|
// whole list is removed
|
||||||
|
this.emit('evicted', cursor.value);
|
||||||
|
this.start = null;
|
||||||
|
this.tail = null;
|
||||||
|
// stop iteration
|
||||||
|
cursor = null;
|
||||||
|
} else if (result === true) {
|
||||||
|
// when TRUE, set match as new tail
|
||||||
|
if (cursor !== this.tail) {
|
||||||
|
this.tail = cursor;
|
||||||
|
cursor.next = null;
|
||||||
|
}
|
||||||
|
// stop iteration
|
||||||
|
cursor = null;
|
||||||
|
} else {
|
||||||
|
// evicted
|
||||||
|
this.emit('evicted', cursor.value);
|
||||||
|
// iterate to next
|
||||||
|
cursor = cursor.prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray () {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (this.start) {
|
||||||
|
let cursor = this.start;
|
||||||
|
while (cursor) {
|
||||||
|
result.push(cursor.value);
|
||||||
|
cursor = cursor.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
toArrayReverse () {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (this.tail) {
|
||||||
|
let cursor = this.tail;
|
||||||
|
while (cursor) {
|
||||||
|
result.push(cursor.value);
|
||||||
|
cursor = cursor.prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
107
packages/unleash-api/lib/client-metrics/list.test.js
Normal file
107
packages/unleash-api/lib/client-metrics/list.test.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('ava');
|
||||||
|
const List = require('./list');
|
||||||
|
|
||||||
|
function getList () {
|
||||||
|
const list = new List();
|
||||||
|
list.add(1);
|
||||||
|
list.add(2);
|
||||||
|
list.add(3);
|
||||||
|
list.add(4);
|
||||||
|
list.add(5);
|
||||||
|
list.add(6);
|
||||||
|
list.add(7);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should emit "evicted" events for objects leaving list', (t) => {
|
||||||
|
const list = getList();
|
||||||
|
const evictedList = [];
|
||||||
|
list.on('evicted', (value) => {
|
||||||
|
evictedList.push(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
t.true(evictedList.length === 0);
|
||||||
|
|
||||||
|
list.reverseRemoveUntilTrue(({ value }) => {
|
||||||
|
if (value === 4) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.true(evictedList.length === 3);
|
||||||
|
|
||||||
|
list.reverseRemoveUntilTrue(() => false);
|
||||||
|
|
||||||
|
t.true(evictedList.length === 7);
|
||||||
|
|
||||||
|
list.add(1);
|
||||||
|
list.reverseRemoveUntilTrue(() => false);
|
||||||
|
|
||||||
|
t.true(evictedList.length === 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list should be able remove until given value', (t) => {
|
||||||
|
const list = getList();
|
||||||
|
|
||||||
|
t.true(list.toArray().length === 7);
|
||||||
|
|
||||||
|
list.reverseRemoveUntilTrue(({ value }) => value === 4);
|
||||||
|
t.true(list.toArray().length === 4);
|
||||||
|
|
||||||
|
list.reverseRemoveUntilTrue(({ value }) => value === 3);
|
||||||
|
t.true(list.toArray().length === 3);
|
||||||
|
|
||||||
|
list.reverseRemoveUntilTrue(({ value }) => value === 3);
|
||||||
|
t.true(list.toArray().length === 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list can be cleared and re-add entries', (t) => {
|
||||||
|
const list = getList();
|
||||||
|
|
||||||
|
list.add(8);
|
||||||
|
list.add(9);
|
||||||
|
|
||||||
|
t.true(list.toArray().length === 9);
|
||||||
|
|
||||||
|
list.reverseRemoveUntilTrue(() => false);
|
||||||
|
|
||||||
|
t.true(list.toArray().length === 0);
|
||||||
|
|
||||||
|
list.add(1);
|
||||||
|
list.add(2);
|
||||||
|
list.add(3);
|
||||||
|
|
||||||
|
t.true(list.toArray().length === 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('should iterate', (t) => {
|
||||||
|
const list = getList();
|
||||||
|
|
||||||
|
let iterateCount = 0;
|
||||||
|
list.iterate(({ value }) => {
|
||||||
|
iterateCount++;
|
||||||
|
if (value === 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
t.true(iterateCount === 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reverse iterate', (t) => {
|
||||||
|
const list = getList();
|
||||||
|
|
||||||
|
let iterateCount = 0;
|
||||||
|
list.iterateReverse(({ value }) => {
|
||||||
|
iterateCount++;
|
||||||
|
if (value === 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
t.true(iterateCount === 5);
|
||||||
|
});
|
@ -1,47 +1,44 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const yallist = require('yallist');
|
const List = require('./list');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
// this list must have entires with sorted ttl range
|
// this list must have entries with sorted ttl range
|
||||||
module.exports = class TTLList extends EventEmitter {
|
module.exports = class FIFOTTLList extends EventEmitter {
|
||||||
constructor () {
|
constructor ({
|
||||||
|
interval = 1000,
|
||||||
|
expireAmount = 1,
|
||||||
|
expireType = 'hours',
|
||||||
|
} = {}) {
|
||||||
super();
|
super();
|
||||||
this.cache = yallist.create();
|
this.expireAmount = expireAmount;
|
||||||
setInterval(() => {
|
this.expireType = expireType;
|
||||||
|
|
||||||
|
this.list = new List();
|
||||||
|
|
||||||
|
this.list.on('evicted', ({ value, ttl }) => {
|
||||||
|
this.emit('expire', value, ttl);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.timer = setInterval(() => {
|
||||||
this.timedCheck();
|
this.timedCheck();
|
||||||
}, 1000);
|
}, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
expire (entry) {
|
add (value, timestamp = new Date()) {
|
||||||
this.emit('expire', entry.value);
|
const ttl = moment(timestamp).add(this.expireAmount, this.expireType);
|
||||||
}
|
this.list.add({ ttl, value });
|
||||||
|
|
||||||
add (value, timestamp) {
|
|
||||||
const ttl = moment(timestamp).add(1, 'hour');
|
|
||||||
this.cache.push({ ttl, value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
timedCheck () {
|
timedCheck () {
|
||||||
const now = moment(new Date());
|
const now = moment(new Date());
|
||||||
// find index to remove
|
this.list.reverseRemoveUntilTrue(({ value }) => now.isBefore(value.ttl));
|
||||||
let done = false;
|
}
|
||||||
// TODO: might use internal linkedlist
|
|
||||||
this.cache.forEachReverse((entry, index) => {
|
destroy () {
|
||||||
if (done) {
|
clearTimeout(this.timer);
|
||||||
return;
|
delete this.timer;
|
||||||
} else if (now.isBefore(entry.ttl)) {
|
this.list = null;
|
||||||
// When we hit a valid ttl, remove next items in list (iteration is reversed)
|
|
||||||
this.cache = this.cache.slice(0, index + 1);
|
|
||||||
done = true;
|
|
||||||
} else if (index === 0) {
|
|
||||||
this.expire(entry);
|
|
||||||
// if rest of list has timed out, let it DIE!
|
|
||||||
this.cache = yallist.create(); // empty=
|
|
||||||
} else {
|
|
||||||
this.expire(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
63
packages/unleash-api/lib/client-metrics/ttl-list.test.js
Normal file
63
packages/unleash-api/lib/client-metrics/ttl-list.test.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('ava');
|
||||||
|
const TTLList = require('./ttl-list');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
test.cb('should emit expire', (t) => {
|
||||||
|
const list = new TTLList({
|
||||||
|
interval: 20,
|
||||||
|
expireAmount: 10,
|
||||||
|
expireType: 'milliseconds',
|
||||||
|
});
|
||||||
|
|
||||||
|
list.on('expire', (entry) => {
|
||||||
|
list.destroy();
|
||||||
|
t.true(entry.n === 1);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
list.add({ n: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb('should slice off list', (t) => {
|
||||||
|
const list = new TTLList({
|
||||||
|
interval: 10,
|
||||||
|
expireAmount: 10,
|
||||||
|
expireType: 'milliseconds',
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.time('4');
|
||||||
|
list.add({ n: '4' }, moment().add(300, 'milliseconds'));
|
||||||
|
|
||||||
|
// console.time('3');
|
||||||
|
list.add({ n: '3' }, moment().add(200, 'milliseconds'));
|
||||||
|
|
||||||
|
// console.time('2');
|
||||||
|
list.add({ n: '2' }, moment().add(50, 'milliseconds'));
|
||||||
|
|
||||||
|
// console.time('1');
|
||||||
|
list.add({ n: '1' }, moment().add(1, 'milliseconds'));
|
||||||
|
|
||||||
|
const expired = [];
|
||||||
|
|
||||||
|
list.on('expire', (entry) => {
|
||||||
|
// console.timeEnd(entry.n);
|
||||||
|
expired.push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
t.true(expired.length === 1);
|
||||||
|
}, 30);
|
||||||
|
setTimeout(() => {
|
||||||
|
t.true(expired.length === 2);
|
||||||
|
}, 71);
|
||||||
|
setTimeout(() => {
|
||||||
|
t.true(expired.length === 3);
|
||||||
|
}, 221);
|
||||||
|
setTimeout(() => {
|
||||||
|
t.true(expired.length === 4);
|
||||||
|
list.destroy();
|
||||||
|
t.end();
|
||||||
|
}, 330);
|
||||||
|
});
|
@ -65,6 +65,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bluebird": "^3.4.6",
|
"bluebird": "^3.4.6",
|
||||||
|
"ava": "^0.16.0",
|
||||||
"coveralls": "^2.11.12",
|
"coveralls": "^2.11.12",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"mocha": "^3.0.2",
|
"mocha": "^3.0.2",
|
||||||
|
Loading…
Reference in New Issue
Block a user