Files
pathfinder/js/app/lib/cache.js
Mark Friedrich a5f29ee2eb - NEW "Thera connections" UI module, closed #829
- Upgraded "[_pathfinder_esi_](https://github.com/exodus4d/pathfinder_esi)" Web API client`v1.3.2` → `v2.0.0`
- Fixed a js bug where current active(selected) system becomes deselected after system was dragged on map
- Fixed a js bug where new auto mapped systems (e.g. after jump) were positioned outside current map scroll viewport
- Fixed a js bug where map sync failed after map tabs switch
- Fixed blurry map when map zoom was changed
- Fixed multiple minor JS bugs where map render/update failed
2020-03-02 16:42:36 +01:00

276 lines
8.2 KiB
JavaScript

define([], () => {
'use strict';
/**
* Abstract Cache Strategy class
* @type {AbstractStrategy}
*/
class AbstractStrategy {
constructor(){
if(new.target === AbstractStrategy){
throw new TypeError('Cannot construct AbstractStrategy instances directly');
}
}
/**
* factory for Strategy* instances
* @returns {AbstractStrategy}
*/
static create(){
return new this();
}
}
/**
* LIFO Cache Strategy - First In First Out
* -> The cache evicts the entries in the order they were added,
* without any regard to how often or how many times they were accessed before.
* @type {StrategyFIFO}
*/
class StrategyFIFO extends AbstractStrategy {
valueToCompare(metaData){
return metaData.age();
}
compare(a, b){
return b - a;
}
}
/**
* LFU Cache Strategy - Least Frequently Used
* -> The cache evicts the entries in order how often have been accessed.
* Those that are used least often are discarded first
* @type {StrategyLFU}
*/
class StrategyLFU extends AbstractStrategy {
valueToCompare(metaData){
return metaData.hitCount;
}
compare(a, b){
return a - b;
}
}
/**
* LRU Cache Strategy - Least Recently Used
* -> The cache evicts entries that have not been used for the longest amount of time.
* No matter how often they have been accessed.
* @type {StrategyLRU}
*/
class StrategyLRU extends AbstractStrategy {
valueToCompare(metaData){
return metaData.hits[metaData.hits.length - 1] || metaData.set;
}
compare(a, b){
return a - b;
}
}
/**
* Each entry in cache also has its own instance of CacheEntryMeta
* -> The configured Cache Strategy use this meta data for eviction policy
* @type {CacheEntryMeta}
*/
class CacheEntryMeta {
constructor(ttl, tSet){
this._ttl = ttl; // ttl < 0 => no expire
this._tSet = tSet || this.constructor.now();
this._tHits = [];
}
get set(){
return this._tSet;
}
get hits(){
return this._tHits;
}
get hitCount(){
return this.hits.length;
}
newHit(current){
this._tHits.push(current || this.constructor.now());
}
age(current){
return (current || this.constructor.now()) - this._tSet;
}
expired(current){
return this._ttl < 0 ? false : this._ttl < this.age(current);
}
static now(){
return new Date().getTime() / 1000;
}
static create(ttl, tSet){
return new this(ttl, tSet);
}
}
/**
* Each instance of Cache represents a key value in memory data store
* -> Name should be set to identify current Cache instance
* -> Default ttl can be overwritten by individual entries
* -> Cache Strategy handles eviction policy
* -> Buffer Size (in percent) can be used to remove e.g. 10% of all entries
* if cache reaches maxSize limit, to increase performance.
* @type {Cache}
*/
class Cache {
constructor(config = {}){
this._config = Object.assign({}, Cache.defaultConfig, config);
this._store = new Map();
this._metaStore = new WeakMap();
this._strategy = this.constructor.setStrategy(this._config.strategy);
this.debug = (msg,...data) => {
if(this._config.debug){
data = (data || []);
data.unshift(this._config.name);
console.debug('debug: CACHE %o | ' + msg, ...data);
}
};
this.debug('New Cache instance');
}
get size(){
return this._store.size;
}
isFull(){
return this.size>= this._config.maxSize;
}
set(key, value, ttl){
if(this._store.has(key)){
this.debug('SET key %o, UPDATE value %o', key, value);
this._store.set(key, value);
}else{
this.debug('SET key %o, NEW value %o', key, value);
if(this.isFull()){
this.debug(' ↪ FULL trim cache…');
this.trim(this.trimCount(1));
}
this._store.set(key, value);
}
this._metaStore.set(value, CacheEntryMeta.create(ttl || this._config.ttl));
}
get(key){
if(this._store.has(key)){
let value = this._store.get(key);
if(value){
let metaData = this._metaStore.get(value);
if(metaData.expired()){
this.debug('EXPIRED key %o delete', key);
this.delete(key);
}else{
this.debug('HIT key %o', key);
metaData.newHit();
return value;
}
}
}
this.debug('MISS key %o', key);
}
getOrDefault(key, def){
return this.get(key) || def;
}
keysForTrim(count){
let trimKeys = [];
let compare = [];
for(let [key, value] of this._store){
let metaData = this._metaStore.get(value);
if(metaData.expired()){
trimKeys.push(key);
if(count === trimKeys.length){
break;
}
}else{
compare.push({
key: key,
value: this._strategy.valueToCompare(metaData)
});
}
}
let countLeft = count - trimKeys.length;
if(countLeft > 0){
compare = compare.sort((a, b) => this._strategy.compare(a.value, b.value));
trimKeys = trimKeys.concat(compare.splice(0, countLeft).map(a => a.key));
}
return trimKeys;
}
keys(){
return this._store.keys();
}
delete(key){
return this._store.delete(key);
}
clear(){
this._store.clear();
}
trimCount(spaceLeft){
let bufferSize = Math.max(Math.round(this._config.maxSize / 100 * this._config.bufferSize), spaceLeft);
return Math.min(Math.max(this.size - this._config.maxSize + bufferSize, 0), this.size);
}
trim(count){
if(count > 0){
let trimKeys = this.keysForTrim(count);
if(count > trimKeys.length){
console.warn(' ↪ Failed to trim(%i) entries. Only %i in store', count, this.size);
}
this.debug(' ↪ DELETE min %i keys: %o', count, trimKeys);
trimKeys.forEach(key => this.delete(key));
}
}
status(){
return {
config: this._config,
store: this._store,
metaStore: this._metaStore
};
}
static setStrategy(name){
switch(name){
case 'FIFO': return StrategyFIFO.create();
case 'LFU': return StrategyLFU.create();
case 'LRU': return StrategyLRU.create();
default:
throw new ReferenceError('Unknown cache strategy name: ' + name);
}
}
}
Cache.defaultConfig = {
name: 'Default', // custom unique name for identification
ttl: 3600, // default ttl for cache entries
maxSize: 600, // max cache entries
bufferSize: 10, // cache entry count in percent to be removed if maxSize reached
strategy: 'FIFO', // cache strategy policy
debug: false // debug output in console
};
return Cache;
});