UNPKG

@linkedmink/multilevel-aging-cache

Version:

Package provides an interface to cache and persist data to Redis, MongoDB, memory

166 lines (139 loc) 4.05 kB
import { RBTree } from 'bintrees'; import { compareAscending, IAgedQueue } from './IAgedQueue'; import { Logger } from '../shared/Logger'; export class FIFOAgedQueue<TKey> implements IAgedQueue<TKey> { private readonly logger = Logger.get(FIFOAgedQueue.name); private readonly ageLimit?: number; private readonly ageTree: RBTree<number> = new RBTree(compareAscending); private readonly ageMap = new Map<TKey, number>(); private readonly ageBuckets = new Map<number, Set<TKey>>(); /** * @param maxEntries The maximum number of entries to store in the cache, undefined for no max * @param ageLimit The maximum time to keep entries in minutes */ constructor(private readonly maxEntries?: number, ageLimit = 200) { this.ageLimit = ageLimit * 1000 * 60; } /** * @param key The key to add * @param age The age if explicitly or default if undefined */ addOrReplace(key: TKey, age?: number): void { const existingAge = this.ageMap.get(key); const newAge = age ? age : this.getInitialAge(key); this.ageMap.set(key, newAge); if (existingAge !== undefined) { this.deleteBucket(existingAge, key); } this.addBucket(newAge, key); } /** * @return The next key in order or null if there is no key */ next(): TKey | null { const minAge = this.ageTree.min(); if (!minAge) { return null; } const minBucket = this.ageBuckets.get(minAge); if (minBucket === undefined) { return null; } const iterator = minBucket.values().next(); return iterator.value as TKey; } /** * @param key The key to delete */ delete(key: TKey): void { const age = this.ageMap.get(key); if (age === undefined) { return; } this.ageMap.delete(key); this.deleteBucket(age, key); } /** * @return True if the next key in order is expired and should be removed */ isNextExpired(): boolean { if (!this.maxEntries && !this.ageLimit) { return false; } if (this.maxEntries && this.ageMap.size > this.maxEntries) { this.logger.debug(`Max Entries Exceeded: ${this.maxEntries}`); return true; } const next = this.next(); if (next === null) { return false; } if (this.ageLimit) { const age = this.ageMap.get(next); if (age !== undefined && age + this.ageLimit < Date.now()) { this.logger.debug(`Age Limit Exceeded: age=${age},limit=${this.ageLimit}`); return true; } } return false; } /** * @param key The key we want a default for * @return The default age that will very by algorithm */ getInitialAge(_key: TKey): number { return Date.now(); } /** * @param key Advance the age of the specified key */ updateAge(key: TKey): void { const oldAge = this.ageMap.get(key); if (oldAge === undefined) { return; } const newAge = Date.now(); this.ageMap.set(key, newAge); this.deleteBucket(oldAge, key); this.addBucket(newAge, key); } /** * @param ageA The first age to compare * @param ageB The second age to compare * @return 0 if same order, positive if ageA after ageB, negative if ageA before ageB */ compare = compareAscending; /** * @return The number of keys in the queue */ size(): number { return this.ageMap.size; } private addBucket(age: number, key: TKey): void { const bucket = this.ageBuckets.get(age); if (bucket === undefined) { this.ageBuckets.set(age, new Set([key])); } else { bucket.add(key); } const found = this.ageTree.find(age); if (found === null) { this.ageTree.insert(age); } } private deleteBucket(age: number, key: TKey): void { const bucket = this.ageBuckets.get(age); if (bucket === undefined) { return; } bucket.delete(key); if (bucket.size > 0) { return; } this.ageBuckets.delete(age); const found = this.ageTree.find(age); if (found !== null) { this.ageTree.remove(age); } } }