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';
 | 
					'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