happn-3
Version:
pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb
189 lines (171 loc) • 5.53 kB
JavaScript
const StaticCache = require('./cache-static');
const LRUCache = require('./cache-lru');
const PersistedCache = require('./cache-persist');
const commons = require('happn-commons');
module.exports = class CacheService extends require('events').EventEmitter {
#caches;
#config;
#statisticsInterval;
#lastStats;
constructor(opts = {}) {
super();
this.initialize = commons.utils.maybePromisify(this.initialize);
this.stop = commons.utils.maybePromisify(this.stop);
if (opts && opts.logger) {
this.log = opts.logger.createLogger('Cache');
} else {
let logger = require('happn-logger');
logger.configure({
logLevel: 'info',
});
this.log = logger.createLogger('Cache');
}
this.log.$$TRACE('construct(%j)', opts);
this.#caches = {};
}
static create(opts) {
return new CacheService(opts);
}
initialize(config, callback) {
try {
if (typeof config === 'function') {
callback = config;
config = {};
}
this.#config = config || {};
this.#caches = {};
if (
typeof this.#config.statisticsInterval === 'number' &&
this.#config.statisticsInterval < 1e3
) {
this.log.warn(`statisticsInterval smaller than a second, ignoring`);
}
if (this.#config.statisticsInterval >= 1e3) this.#startLoggingStatistics();
callback();
} catch (e) {
callback(e);
}
}
create(name, opts = {}) {
const overrides = this.#config.overrides || {};
opts.cache = overrides[name] || opts.cache || {};
opts.type = opts.type || commons.constants.CACHE_TYPES.STATIC;
if (this.#caches[name] && !opts.overwrite) {
throw new Error('a cache by this name already exists');
}
const type = opts.type.toLowerCase();
if (Object.values(commons.constants.CACHE_TYPES).indexOf(type) === -1) {
throw new Error(`unknown cache type: ${type}`);
}
this.#caches[name] = { type };
let instance;
if (type === commons.constants.CACHE_TYPES.LRU) {
instance = new LRUCache(name, opts.cache);
} else if (type === commons.constants.CACHE_TYPES.PERSIST) {
opts.cache.key_prefix = name;
instance = new PersistedCache(name, opts.cache);
} else {
instance = new StaticCache(name, opts.cache);
}
this.#caches[name] = { type, instance };
Object.defineProperty(this.#caches[name], 'utilities', {
value: this.happn.services.utils,
});
return this.#caches[name].instance;
}
async clearAll(deleteOnClear) {
for (const name of Object.keys(this.#caches)) {
await this.clear(name, deleteOnClear);
}
}
async clear(name, deleteOnClear = false) {
let found = this.#caches[name];
if (!found) return;
await found.instance.clear();
this.emit('cache-cleared', name);
if (deleteOnClear) {
// dont clear again, so clearOnDelete false
this.delete(name, false);
}
}
delete(name, clearOnDelete = true) {
const toDelete = this.#caches[name];
if (!toDelete) return;
delete this.#caches[name];
if (clearOnDelete) {
toDelete.clear();
}
this.emit('cache-deleted', name);
}
stopAll() {
Object.values(this.#caches).forEach((cache) => cache.instance.stop());
}
stop(options, callback) {
if (typeof options === 'function') {
callback = options;
}
this.stopAll();
if (this.#statisticsInterval) this.#stopLoggingStatistics();
if (typeof callback === 'function') {
callback();
}
}
getStats() {
return Object.values(this.#caches).reduce((stats, cache) => {
const cacheStats = commons._.merge(cache.instance.stats(), { type: cache.type });
stats[cache.instance.name] = cacheStats;
return stats;
}, {});
}
getCache(name) {
return this.#caches[name];
}
getOrCreate(name, opts) {
const found = this.getCache(name);
if (found) return found.instance;
return this.create(name, opts);
}
#startLoggingStatistics() {
this.#statisticsInterval = setInterval(() => {
try {
const stats = this.getStats();
this.log.json.info(
commons._.merge(this.#calculatePerSecond(stats), {
timestamp: Date.now(),
name: this.happn.name,
}),
'cache-service-statistics'
);
this.#lastStats = stats;
} catch (e) {
this.log.warn(`failure logging statistics: ${e.message}`);
}
}, this.#config.statisticsInterval);
}
#calculatePerSecond(stats) {
return Object.keys(stats).reduce((calculated, statsKey) => {
if (!this.#lastStats) {
calculated[statsKey] = commons._.merge(stats[statsKey], {
hitsPerSec: stats[statsKey].hits / (this.#config.statisticsInterval / 1e3),
missesPerSec: stats[statsKey].misses / (this.#config.statisticsInterval / 1e3),
});
return calculated;
}
if (!this.#lastStats[statsKey]) {
this.#lastStats[statsKey] = { hits: 0, misses: 0 };
}
calculated[statsKey] = commons._.merge(stats[statsKey], {
hitsPerSec:
(stats[statsKey].hits - this.#lastStats[statsKey].hits) /
(this.#config.statisticsInterval / 1e3),
missesPerSec:
(stats[statsKey].misses - this.#lastStats[statsKey].misses) /
(this.#config.statisticsInterval / 1e3),
});
return calculated;
}, {});
}
#stopLoggingStatistics() {
clearInterval(this.#statisticsInterval);
}
};