1
0
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:
sveisvei 2016-11-06 20:52:08 +01:00 committed by Ivar Conradi Østhus
parent f904781d61
commit debdfee9c9
5 changed files with 332 additions and 32 deletions

View 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;
}
};

View 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);
});

View File

@ -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;
}
};

View 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);
});

View File

@ -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",