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';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const yallist = require('yallist');
|
||||
const List = require('./list');
|
||||
const moment = require('moment');
|
||||
|
||||
// this list must have entires with sorted ttl range
|
||||
module.exports = class TTLList extends EventEmitter {
|
||||
constructor () {
|
||||
// this list must have entries with sorted ttl range
|
||||
module.exports = class FIFOTTLList extends EventEmitter {
|
||||
constructor ({
|
||||
interval = 1000,
|
||||
expireAmount = 1,
|
||||
expireType = 'hours',
|
||||
} = {}) {
|
||||
super();
|
||||
this.cache = yallist.create();
|
||||
setInterval(() => {
|
||||
this.expireAmount = expireAmount;
|
||||
this.expireType = expireType;
|
||||
|
||||
this.list = new List();
|
||||
|
||||
this.list.on('evicted', ({ value, ttl }) => {
|
||||
this.emit('expire', value, ttl);
|
||||
});
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.timedCheck();
|
||||
}, 1000);
|
||||
}, interval);
|
||||
}
|
||||
|
||||
expire (entry) {
|
||||
this.emit('expire', entry.value);
|
||||
}
|
||||
|
||||
add (value, timestamp) {
|
||||
const ttl = moment(timestamp).add(1, 'hour');
|
||||
this.cache.push({ ttl, value });
|
||||
add (value, timestamp = new Date()) {
|
||||
const ttl = moment(timestamp).add(this.expireAmount, this.expireType);
|
||||
this.list.add({ ttl, value });
|
||||
}
|
||||
|
||||
timedCheck () {
|
||||
const now = moment(new Date());
|
||||
// find index to remove
|
||||
let done = false;
|
||||
// TODO: might use internal linkedlist
|
||||
this.cache.forEachReverse((entry, index) => {
|
||||
if (done) {
|
||||
return;
|
||||
} else if (now.isBefore(entry.ttl)) {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
this.list.reverseRemoveUntilTrue(({ value }) => now.isBefore(value.ttl));
|
||||
}
|
||||
|
||||
destroy () {
|
||||
clearTimeout(this.timer);
|
||||
delete this.timer;
|
||||
this.list = null;
|
||||
}
|
||||
};
|
||||
|
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": {
|
||||
"bluebird": "^3.4.6",
|
||||
"ava": "^0.16.0",
|
||||
"coveralls": "^2.11.12",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^3.0.2",
|
||||
|
Loading…
Reference in New Issue
Block a user