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
371 lines (307 loc) • 9.69 kB
JavaScript
module.exports = PersistedCache;
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var lt = require('long-timeout');
var Promise = require('bluebird');
var sift = require('sift').default;
PersistedCache.prototype.has = has;
PersistedCache.prototype.set = Promise.promisify(set);
PersistedCache.prototype.get = Promise.promisify(get);
PersistedCache.prototype.increment = Promise.promisify(increment);
PersistedCache.prototype.update = Promise.promisify(update);
PersistedCache.prototype.remove = Promise.promisify(remove);
PersistedCache.prototype.clear = Promise.promisify(clear);
PersistedCache.prototype.stop = stop;
PersistedCache.prototype.all = Promise.promisify(all);
PersistedCache.prototype.__returnAll = __returnAll;
PersistedCache.prototype.sync = Promise.promisify(sync);
PersistedCache.prototype.on = on;
PersistedCache.prototype.off = off;
PersistedCache.prototype.appendTimeout = appendTimeout;
PersistedCache.prototype.clearTimeout = _clearTimeout;
PersistedCache.prototype.__emit = __emit;
PersistedCache.prototype.__getData = __getData;
PersistedCache.prototype.__persistData = __persistData;
PersistedCache.prototype.__removeData = __removeData;
PersistedCache.prototype.__tryCallback = __tryCallback;
PersistedCache.prototype.__setCallback = __setCallback;
PersistedCache.prototype.__all = __all;
function PersistedCache(opts) {
if (!opts.dataStore) throw new Error('no dataStore defined for a persisted cache');
this.dataStore = opts.dataStore;
this.__cache = {};
this.__eventEmitter = new EventEmitter();
if (!opts.key_prefix) opts.key_prefix = '/_SYSTEM/_CACHE/default';
else opts.key_prefix = '/_SYSTEM/_CACHE/' + opts.key_prefix;
this.opts = opts;
this.__timeouts = {};
}
function has(key) {
return this.__cache[key] != null;
}
function __setCallback(key, cacheItem, callback) {
this.__cache[key] = cacheItem;
this.__emit('item-set', cacheItem);
return this.__tryCallback(callback, cacheItem, null);
}
function set(key, data, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = null;
}
if (!opts) opts = {};
if (!opts.ttl) opts.ttl = this.opts.defaultTTL;
var cacheItem = {
data: this.utilities.clone(data),
key: key
};
// eslint-disable-next-line
if (opts.ttl > 0 && opts.ttl != Infinity) this.appendTimeout(cacheItem, opts.ttl);
if (opts.noPersist) return this.__setCallback(key, cacheItem, callback);
this.__persistData(key, cacheItem, e => {
if (e) return this.__tryCallback(callback, null, e);
this.__setCallback(key, cacheItem, callback);
});
}
function get(key, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
if (!opts) opts = {};
let cached = this.__cache[key];
if (cached != null) return this.__tryCallback(callback, cached.data, null, true);
if (this.__synced) return this.__tryCallback(callback, null, null); //we are in sync with the db, null value
this.__getData(key, (e, item) => {
if (e) return callback(e);
if (item) {
return this.set(
key,
item.data.data,
{
ttl: item.data.ttl,
noPersist: true
},
e => {
this.__tryCallback(callback, item, e, true);
}
);
}
if (opts.retrieveMethod) {
return opts.retrieveMethod.call(opts.retrieveMethod, (e, result) => {
if (e) return callback(e);
// -1 and 0 are perfectly viable things to cache
if (result == null) return this.__tryCallback(callback, null, null);
this.set(key, result, opts, e => {
return this.__tryCallback(callback, result, e, true);
});
});
}
if (opts.default) {
var value = opts.default.value;
delete opts.default.value;
return this.set(key, value, opts.default, e => {
return this.__tryCallback(callback, value, e, true);
});
}
return this.__tryCallback(callback, null, null);
});
}
function increment(key, by, callback) {
var _this = this;
if (_this.__cache[key] && typeof _this.__cache[key].data === 'number') {
_this.__cache[key].data += by;
var dataItem = {
data: this.__cache[key].data,
key: key
};
return _this.__persistData(key, dataItem, function(e) {
if (e) _this.__tryCallback(callback, null, e);
else _this.__tryCallback(callback, _this.__cache[key].data, null);
});
}
return this.__tryCallback(callback, null, null);
}
function update(key, data, callback) {
var _this = this;
try {
if (_this.__cache[key]) {
_this.__cache[key].data = data;
return _this.__persistData(key, _this.__cache[key], function(e) {
if (e) return _this.__tryCallback(callback, null, e);
_this.__tryCallback(callback, _this.__cache[key], null);
});
} else _this.__tryCallback(callback, null, null);
} catch (e) {
return this.__tryCallback(callback, null, e);
}
}
function remove(key, opts, callback) {
var _this = this;
if (typeof opts === 'function') {
callback = opts;
opts = null;
}
if (!opts) opts = {};
var foundItem = false;
if (_this.__cache[key]) {
if (opts.noPersist) {
delete _this.__cache[key];
_this.__emit('item-removed', key);
return _this.__tryCallback(callback, true, null);
}
return _this.__removeData(key, function(e) {
if (e) return callback(e);
delete _this.__cache[key];
_this.__emit('item-removed', key);
_this.__tryCallback(callback, true, null);
});
}
_this.__tryCallback(callback, foundItem, null);
}
function stop() {
Object.keys(this.__timeouts).forEach(key => {
this.clearTimeout(key);
});
}
function clear(callback) {
this.stop();
this.dataStore.remove(this.opts.key_prefix + '/*', {}, e => {
if (e) {
if (callback) return callback(e);
throw e;
}
this.__cache = {};
if (callback) callback();
});
}
function all(filter, callback) {
if (typeof filter === 'function') {
callback = filter;
filter = null;
}
this.__all(function(e, items) {
if (e) return callback(e);
if (filter)
return callback(
null,
sift(
{
$and: [filter]
},
items
)
);
else return callback(null, items);
});
}
function sync(callback) {
var _this = this;
_this.dataStore.get(this.opts.key_prefix + '/*', {}, function(e, items) {
if (e) return callback(e);
if (!items || items.length === 0) {
_this.__synced = true;
return callback(null);
}
async.eachSeries(
items,
function(item, itemCB) {
if (item.data.ttl) {
if (Date.now() - item._meta.modified > item.data.ttl) {
return _this.__removeData(item.data.key, itemCB);
}
}
_this.set(
item.data.key,
item.data.data,
{
ttl: item.data.ttl,
noPersist: true
},
itemCB
);
},
function(e) {
if (e) return callback(e);
_this.__synced = true;
callback();
}
);
});
}
function on(key, handler) {
return this.__eventEmitter.on(key, handler);
}
function off(key, handler) {
return this.__eventEmitter.removeListener(key, handler);
}
function appendTimeout(data, ttl) {
const dataKey = data.key;
this.clearTimeout(dataKey);
this.__timeouts[dataKey] = lt.setTimeout(() => {
this.remove(dataKey, e => {
if (e) this.__emit('error', new Error('failed to remove timed out item'));
this.__emit('item-timed-out', {
key: dataKey
});
});
}, ttl);
data.ttl = ttl;
}
function _clearTimeout(key) {
lt.clearTimeout(this.__timeouts[key]);
delete this.__timeouts[key];
}
function __returnAll(callback) {
var allItems = [];
Object.keys(this.__cache).forEach(itemKey => {
allItems.push(this.utilities.clone(this.__cache[itemKey].data));
});
callback(null, allItems);
}
function __all(callback) {
var _this = this;
if (!_this.__synced)
_this.sync(function(e) {
if (e) return callback(e);
_this.__returnAll(callback);
});
else _this.__returnAll(callback);
}
function __emit(key, data) {
return this.__eventEmitter.emit(key, data);
}
function __getData(key, callback) {
this.dataStore.get(this.opts.key_prefix + '/' + key, {}, (e, item) => {
if (e) return callback(e);
if (item && item.data.ttl && Date.now() - item._meta.modified > item.data.ttl)
return this.__removeData(item.data.key, e => {
if (e) return callback(e);
return callback(null, null);
});
return callback(null, item);
});
}
function __persistData(key, data, callback) {
this.dataStore.upsert(
this.opts.key_prefix + '/' + key,
data,
{
merge: true
},
callback
);
}
function __removeData(key, callback) {
this.dataStore.remove(this.opts.key_prefix + '/' + key, {}, callback);
}
function __tryCallback(callback, data, e, clone) {
var callbackData = data;
if (data && clone) callbackData = this.utilities.clone(data);
if (e) {
if (callback) return callback(e);
else throw e;
}
if (callback) callback(null, callbackData);
else return callbackData;
}