Source: lib/server.js

/**
 * @description
 * This module defines an interface to be used by the server, on its own
 * behalf, to put/delete items from redis server DB.
 */


"use strict";


// Exporting the Constructor
exports = module.exports = Server;
exports.Server = Server;


// npm-installed modules
var _ = require("lodash");
var debug = require("debug")("ss-interface:server");
var stringify = require("json-stable-stringify");


// own modules
var defaults = require("./defaults");


/**
 * Create a new instance of the server interface.
 *
 * @constructor
 * @public
 *
 * @param {RedisClient} client
 * @param {Object} [config] - configuration values. See {@link Server#_configure}
 */
function Server(client, config) {
    debug("constructing new server client");
    this._client = client;
    this._minsize = null;
    this._maxsize = null;
    this._key = null;
    this._stringify = null;
    this._reducing = false;
    this._configure(config);
    return this;
}


/**
 * Configure the instance
 *
 * @private
 *
 * @param {Object} [config] - configuration values
 * @param {String} [config.key] - key to use in Redis
 * @param {Number} [config.min_size] - minimum size of a filled cache
 * @param {Number} [config.max_size] - maximum size of a filled cache. If
 *  '+Infinity' is passed, the cache can grow without limit
 * @param {Function} [config.stringify] - function for stringifying objects
 * @param {Boolean} [config.uniqueIds] - guarantee unique item IDs.
 *  WARNING: This works by firing a 'delete' request before adding an item,
 *  thus more requests are processed by Redis. Also, in case of `.add()`,
 *  the smallest and largest item IDs are used as 'min' and 'max',
 *  respectively, when removing from the set.
 */
Server.prototype._configure = function _configure(config) {
    debug("configuring client");
    config = config || { };
    _.defaults(config, defaults, {
        stringify: stringify,
        uniqueIds: false,
    });
    this._minsize = config.min_size;
    this._maxsize = config.max_size;
    this._key = config.key;
    this._stringify = config.stringify;
    this._uniqueIds = config.uniqueIds;
    return this;
};


/**
 * Return the size of the cache i.e. number of items in the cache. Note that
 * this function queries the redis server to determine this size and it is
 * therefore not stored in process memory. This ensure a consistent value
 * across multiple application instances using a single redis server DB.
 *
 * @public
 * @param {Function} callback - callback(err, size)
 */
Server.prototype.getSize = function(done) {
    var self = this;
    self._client.zcard(self._key, function(err, cardinality) {
        if (err) {
            return done(err);
        }
        return done(null, cardinality);
    });
    return self;
};


/**
 * Add one item to the cache. Ths ID is represents its position in the
 * ordered sequence of all items.
 *
 * @public
 *
 * @param {Number} [id] - id of the item
 * @param {Object|String} item - the item itself
 * @param {Function} [callback] - callback(err)
 */
Server.prototype.addOne = function addOne(id, item, callback) {
    if (!callback && _.isFunction(item)) {
        callback = item;
        item = id;
    }

    var self = this;

    if (_.isPlainObject(id)) {
        item = id;
        id = item.id;
    }

    if (!_.isString(item)) {
        item = this._stringify(item);
    }

    // we need to remove the old item to guarantee unique ids
    if (this._uniqueIds) {
        return this._client.zremrangebyscore(this._key, id, id, function(error) {
            if (error) {
                return callback(error);
            }
            return exec();
        });
    }

    return exec();
    function exec() {
        debug("adding item to cache [%s] {%d}", self._key, id);
        self._client.zadd(self._key, id, item, self._wrapCallback(callback));
    }
};


/**
 * Add several items to the cache. These <items> is an array of object,
 * each with an ID (.id prop) that defines its position in the ordered
 * sequence of all items.
 *
 * @public
 *
 * @param {Object[]} items - array of items
 * @param {Number} items[].id - id of the item
 * @param {Function} [callback] - callback(err)
 */
Server.prototype.add = function add(items, callback) {
    debug("adding new items to cache: [%s]", this._key);
    if (items.length === 0) {
        if (callback) {
            return callback(null);
        }
        return null;
    }

    var self = this;
    var args = [this._key];
    var len = items.length;
    var item;
    var min = items[0].id, max = items[items.length - 1].id;

    for (var index = 0; index < len; index++) {
        item = items[index];
        if (item.id < min) min = item.id;
        if (item.id > max) max = item.id;
        args.push(item.id, this._stringify(item));
    }
    args.push(this._wrapCallback(callback));
    // fire a delete request, if we are to guarantee unique ids
    if (this._uniqueIds) {
        return this._client.zremrangebyscore(this._key, min, max, function(error) {
            if (error) return callback(error);
            return exec();
        });
    }
    return exec();
    function exec() {
        return self._client.zadd.apply(self._client, args);
    }
};


/**
 * Remove one item from the cache. This ID represents its position in the
 * ordered sequence of all items.
 *
 * @public
 *
 * @param {Number} id - id of the item
 * @param {Function} [callback] - callback(err)
 */
Server.prototype.removeOne = function removeOne(id, callback) {
    debug("removing item from cache [%s] {%d}", this._key, id);
    this._client.zremrangebyscore(this._key, id, id, callback);
    return;
};


/**
 * Wrap callback for user. This allows us to add several hooks in one place,
 * to be executed just before we return the response to the callee. These
 * hooks include reducing the cache (when necessary).
 *
 * @private
 *
 * @param {Function} [callback] - user's callback
 * @return {Function} wrapped callback
 */
Server.prototype._wrapCallback = function _wrapCallback(callback) {
    var self = this;
    return function(err, added) {
        self._reduce();
        if (callback) {
            callback(err, added);
        }
        return;
    };
};


/**
 * Reduce the cache size to respect maximum limit. This means that at any
 * time, the maximum size the cache can grow to is <config.max_size>.
 * Whenever the size of the cache reaches this upper bound, it is reduced to
 * <config.min_size>. This helps avoid requests that will return 0 items
 * after a size reduction. This function queries the cache on each request.
 * A possible speed up would be have the cache alert the application when
 * maximum size is reached, thus calling this function only when it is
 * needed (and not on each request to add items to cache). Actually, it
 * might happen to fewer times than that as we are debouncing requests in
 * this app process.
 * All functions adding items to the cache call this function thus ensuring
 * the cache does not grow out of bounds.
 *
 * @todo handle errors that are being ignored
 *
 * @private
 */
Server.prototype._reduce = function _reduce() {
    var self = this;

    // this helps debounce requests in this application.
    if (self._reducing) {
        return;
    }

    // if the max_size is +Infinity, then we are NEVER reduce cache size
    if (self._maxsize === +Infinity) {
        return;
    }

    self._reducing = true;
    self.getSize(function(err, currentSize) {
        if (err) {
            // just ignore this error
            return;
        }
        // if the maximum size has not been reached
        if (currentSize < self._maxsize) {
            self._reducing = false;
            return;
        }

        // maximum size has been reached, so we have to reduce items
        debug("reducing size of cache '%s'", self._key);
        self._client.zremrangebyrank(self._key, 0, -(self._minsize + 1), function(reductionErr) {
            if (reductionErr) {
                // IGNORE
            }
            self._reducing = false;
        });
        return;
    });
    return;
};


/**
 * Purge the cache. This destroys your cache's items and its existence in the
 * backing store. This might be useful if your want
 * to reclaim memory in Redis. This function is DANGEROUS. How? If on app
 * startup, we decide to be purging the cache, it would mean that anytime
 * a new application is started, the cache is purged. Such random purges
 * may cause host instability due to the numerous creation and purges
 * requests (especially if we are starting multiple instances at once).
 * It is therefore recommended that you only purge cache when you intend to
 * do so. For example, from an admin panel, you can purge the cache (maybe
 * the cache for the host is misbehaving or during debugging).
 *
 * @public
 *
 * @param {Function} [callback] - callback(err)
 */
Server.prototype.purge = function purgeCache(callback) {
    debug("purging cache [%s]", this._key);
    callback = callback || function() {};
    return this._client.del(this._key, callback);
};