mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	add custom linkedlist-impl
This commit is contained in:
		
							parent
							
								
									4aa97b159c
								
							
						
					
					
						commit
						e5c3495463
					
				
							
								
								
									
										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