apostrophe
Version:
The Apostrophe Content Management System.
247 lines (230 loc) • 8.89 kB
JavaScript
// A general purpose cache implementation for improved performance in all areas
// where results can be retained and reused temporarily. Any number of distinct cache
// objects can be created, identified by distinct names. The standard implementation
// is powered by a MongoDB collection, however it is straightforward to extend this
// module with a different implementation for some or all caches by overriding
// its `get` method.
var async = require('async');
var Promise = require('bluebird');
module.exports = {
alias: 'caches',
singletonWarningIfNot: 'apostrophe-caches',
afterConstruct: function(self, callback) {
self.addClearCacheTask();
return self.getCollection(function(err) {
if (err) {
return callback(err);
}
self.on('apostrophe:migrate', 'ensureIndexesPromisified', function() {
return require('bluebird').promisify(self.ensureIndexes)();
});
return callback(null);
});
},
construct: function(self, options) {
// **SLOW DOWN - READ CAREFULLY!**
//
// THIS IS NOT THE METHOD YOU CALL TO GET A VALUE - THIS IS
// THE METHOD YOU CALL TO **GET A CACHE IN WHICH YOU CAN GET AND SET
// VALUES.** Call it with a name that uniquely identifies
// your **entire cache**, like `weather-data` or similar. The
// object it **returns** has `get` and `set` methods for actual data,
// **as described below**.
//
// `get('cachename')` returns a cache object to store things in
// temporarily. If you call `get` many times with the same cache name,
// you get the same cache object each time.
//
// CACHES MUST NEVER BE RELIED UPON TO STORE INFORMATION. They are a
// performance enhancement ONLY and data may DISAPPEAR at any time.
//
// HOW TO ACTUALLY CACHE DATA: **Every cache object has `.get(key, callback)` and
// `.set(key, value, lifetime, callback)` methods to get
// and store values in the cache.** If you call without a callback,
// a promise is returned.
//
// Example (with promises):
//
// ```
// // Get a cache for weather data, keyed by zip code
// var myCache = self.apos.caches.get('weather-data');
//
// // Store something in the cache
// myCache.set('19147', { clouds: 'cumulus' }, 86400).then(function() { ... })
//
// // Get a value from the cache
// myCache.get('19147').then(function(data) { ... })
// ```
//
// The data to be stored must be representable as JSON for compatibility with
// different implementations. You do NOT have to stringify it yourself.
//
// The `.get` method of each cache object invokes its callback with `(null, value)` in the event
// of success. If the key does not exist in the cache, `value`
// is `undefined`. This is *not* considered an error. If there is no callback
// a promise is returned, which resolves to the cached value or `undefined`.
//
// The `lifetime` argument of `.set` is in seconds and may be omitted
// entirely, in which case data is kept indefinitely (but NOT forever,
// remember that caches can be erased at ANY time, they are not for permanent data storage).
//
// The default implementation is a single MongoDB collection with a
// `name` property to keep the caches separate, but this
// can be swapped out by overriding the `get` method.
//
// Caches also have a `clear()` method to clear the cache. If
// no callback is passed, it returns a promise.
//
// CACHES MUST NEVER BE RELIED UPON TO STORE INFORMATION. They are a
// performance enhancement ONLY and data may DISAPPEAR at any time.
self.get = function(name) {
if (!self.caches) {
self.caches = {};
}
if (!self.caches[name]) {
self.caches[name] = self.constructCache(name);
}
return self.caches[name];
};
self.constructCache = function(name) {
return {
// Fetch an item from the cache. If the item is in the
// cache, the callback receives (null, item). If the
// item is not in the cache the callback receives (null).
// If an error occurs the callback receives (err).
// If there is no callback a promise is returned.
get: function(key, callback) {
if (callback) {
return body(callback);
} else {
return Promise.promisify(body)();
}
function body(callback) {
return self.cacheCollection.findOne({
name: name,
key: key
}, function(err, item) {
if (err) {
return callback(err);
}
if (!item) {
return callback(null);
}
// MongoDB's expireAfterSeconds mechanism isn't instantaneous, so we
// should still enforce this at get() time
if (item.expires && (item.expires < (new Date()))) {
return callback(null);
}
return callback(null, item.value);
});
}
},
// Store an item in the cache. `value` may be any JSON-friendly
// value, including an object. `lifetime` is in seconds.
//
// The callback receives (err).
//
// You may also call with just three arguments:
// key, value, callback. In that case there is no hard limit
// on the lifetime, however NEVER use a cache for PERMANENT
// storage of data. It might be cleared at any time.
//
// If there is no callback a promise is returned.
set: function(key, value, lifetime, callback) {
if (arguments.length === 2) {
lifetime = 0;
return Promise.promisify(body)();
} else if (arguments.length === 3) {
if (typeof (lifetime) === 'function') {
callback = lifetime;
lifetime = 0;
return body(callback);
} else {
return Promise.promisify(body)();
}
} else {
return body(callback);
}
function body(callback) {
var action = {};
var set = {
name: name,
key: key,
value: value
};
action.$set = set;
var unset = {};
if (lifetime) {
set.expires = new Date(new Date().getTime() + lifetime * 1000);
} else {
unset.expires = 1;
action.$unset = unset;
}
return self.cacheCollection.update(
{
name: name,
key: key
},
action,
{
upsert: true
}, callback
);
}
},
// Empty the cache. If there is no callback a promise is returned.
clear: function(callback) {
if (callback) {
return body(callback);
} else {
return Promise.promisify(body)();
}
function body(callback) {
return self.cacheCollection.remove({
name: name
}, callback);
}
}
};
};
self.getCollection = function(callback) {
return self.apos.db.collection('aposCache', function(err, collection) {
if (err) {
return callback(err);
}
self.cacheCollection = collection;
return callback(null);
});
};
self.ensureIndexes = function(callback) {
return async.series({
keyIndex: function(callback) {
return self.cacheCollection.ensureIndex({ key: 1, cache: 1 }, { unique: true }, callback);
},
expireIndex: function(callback) {
return self.cacheCollection.ensureIndex({ expires: 1 }, { expireAfterSeconds: 0 }, callback);
}
}, callback);
};
self.clearCacheTask = function(argv, callback) {
var cacheNames = argv._.slice(1);
if (!cacheNames.length) {
return callback('A cache name or names must be given.');
}
return async.eachSeries(cacheNames, function(name, callback) {
return self.get(name).clear(callback);
}, callback);
};
self.addClearCacheTask = function() {
self.apos.tasks.add('apostrophe-caches', 'clear',
'Usage: node app apostrophe-caches:clear cachename cachename2...\n\n' +
'Clears caches by name. If you are using apos.caches in your own code you will\n' +
'know the cache name. Standard caches include "apostrophe-migrations",\n' +
'"minify" and "oembed". Normally it is not necessary to clear them.',
function(apos, argv, callback) {
return self.clearCacheTask(argv, callback);
}
);
};
}
};