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