LivePersonInc/cacherjs

View on GitHub
src/cacher.js

Summary

Maintainability
F
3 days
Test Coverage
;(function (root, factory) {
    "use strict";

    /* istanbul ignore if */
    //<amd>
    if ("function" === typeof define && define.amd) {

        // AMD. Register as an anonymous module.
        define("cacher", ["exports"], function () {
            if (!root.Cacher) {
                factory(root);
            }

            return root.Cacher;
        });
        return;
    }
    //</amd>
    /* istanbul ignore else */
    if ("object" === typeof exports) {
        // CommonJS
        factory(exports);
    }
    else {
        factory(root);
    }
}(typeof CacherRoot === "undefined" ? this : CacherRoot , function (root) {
    "use strict";

    /*jshint validthis:true */

    /**
     * Cacher constructor
     * @constructor
     * @param {Object} [options] - the configuration options for the instance
     * @param {Number} [options.max] - optional max items in cache
     * @param {Number} [options.maxStrategy] - optional strategy for max items (new items will not be added or closest ttl item should be removed)
     * @param {Number} [options.ttl] - optional TTL for each cache item
     * @param {Number} [options.interval] - optional interval for eviction loop
     * @param {Function} [options.ontimeout] - optional global handler for timeout of items in cache - return false if you want the items to not be deleted after ttl, or object { ttl: number, callback: function } to update the TTL or callback
     * @param {Function} [options.onkickout] - optional global handler for kick out (forced evict) of items in cache
     * @param {Array} [options.stores] - optional array of stores by priority
     * @param {Function} [options.oncomplete] - optional callback for loading completion
     */
    function Cacher(options) {
        // For forcing new keyword
        if (false === (this instanceof Cacher)) {
            return new Cacher(options);
        }

        this.initialize(options);
    }

    Cacher.MAX_STRATEGY = {
        NO_ADD: 0,
        CLOSEST_TTL: 1
    };

    Cacher.prototype = (function () {
        /**
         * Method for initialization
         * @param {Object} [options] - the configuration options for the instance
         * @param {Number} [options.max] - optional max items in cache
         * @param {Number} [options.maxStrategy] - optional strategy for max items (new items will not be added or closest ttl item should be removed)
         * @param {Number} [options.ttl] - optional TTL for each cache item
         * @param {Number} [options.interval] - optional interval for eviction loop
         * @param {Function} [options.ontimeout] - optional global handler for timeout of items in cache - return false if you want the items to not be deleted after ttl, or object { ttl: number, callback: function } to update the TTL or callback
         * @param {Function} [options.onkickout] - optional global handler for kick out (forced evict) of items in cache
         * @param {Array} [options.stores] - optional array of stores by priority
         * @param {Function} [options.oncomplete] - optional callback for loading completion
         */
        function initialize(options) {
            var that = this;
            var stop = false;
            var index = 0;

            function addItem(err, item) {
                that.nostore = true;
                set.call(that, item.key, item.value, item.ttl);
                delete that.nostore;
            }

            if (!this.initialized) {
                options = options || {};

                this.cache = {};                                                                                                               // Objects cache
                this.length = 0;                                                                                                               // Amount of items in cache
                this.maxStrategy = options.maxStrategy || Cacher.MAX_STRATEGY.NO_ADD;                                                          // The strategy to use when max items in cache
                this.max = options.max && !isNaN(options.max) && 0 < options.max ? parseInt(options.max, 10) : 0;                              // Maximum items in cache - 0 for unlimited
                this.ttl = options.ttl && !isNaN(options.ttl) && 0 < options.ttl ? parseInt(options.ttl, 10) : 0;                              // Time to leave for items (this can be overidden for specific items using the set method - 0 for unlimited
                this.interval = options.interval && !isNaN(options.interval) && 0 < options.interval ? parseInt(options.interval, 10) : 1000;  // Interval for running the eviction loop
                this.ontimeout = "function" === typeof options.ontimeout ? options.ontimeout : function () {};                                 // Callback for timeout of items
                this.onkickout = "function" === typeof options.onkickout ? options.onkickout : function () {};                                 // Callback for kickout of items
                this.stores = options.stores || [];

                while (index < this.stores.length && !stop) {
                    if (this.stores[index].autoload && this.stores[index].load) {
                        stop = true;
                        this.stores[index].load({
                            onitem: addItem,
                            oncomplete: options.oncomplete
                        });
                    }

                    index++;
                }

                this.initialized = true;

                _evict.call(this);
            }
        }

        /**
         * Method for getting an item from the cache
         * @param {String} key - the key for the item
         * @param {Boolean} [pop = false] - a boolean flag indicating whether to also pop/remove the item from cache
         * @returns {Object} the item from cache
         */
        function get(key, pop) {
            var item = pop ? remove.call(this, key) : this.cache && this.cache[key] && this.cache[key].item;
            return item;
        }

        /**
         * Method for touching an item in the cache (update its TTL/callback)
         * @param {String} key - the key for the item to be cached
         * @param {Number} [ttl] - the time to live for the item inside the cache
         * @param {Function} [callback] - optional callback to be called on item timeout - return false if you want the item to not be deleted after ttl
         */
        function touch(key, ttl, callback) {
            var item = get.call(this, key, true);
            return _insert.call(this, key, item, ttl, callback);
        }

        /**
         * Method for setting an item to the cache
         * @param {String} key - the key for the item to be cached
         * @param {Object} item - the item to cache
         * @param {Number} [ttl] - the time to live for the item inside the cache
         * @param {Function} [callback] - optional callback to be called on item timeout - return false if you want the item to not be deleted after ttl, or object { ttl: number, callback: function } to update the TTL or callback
         */
        function set(key, item, ttl, callback) {
            return _insert.call(this, key, item, ttl, callback);
        }

        /**
         * Method for removing an item from the cache
         * @param {String} key - the key for the item to be removed
         * @returns {Object} the item that was removed from cache
         */
        function remove(key) {
            var item = this.cache && this.cache[key] && this.cache[key].item;

            if (item) {
                this.cache[key].item = null;
                this.cache[key].callback = null;
                this.cache[key].timeout = null;
                this.cache[key] = null;
                delete this.cache[key];
                this.length--;

                _syncStores.call(this, "remove", key);
            }

            return item;
        }

        /**
         * Method for removing all items from the cache
         */
        function removeAll() {
            if (this.length) {
                for (var key in this.cache) {
                    if (this.cache.hasOwnProperty(key)) {
                        remove.call(this, key);
                    }
                }
            }
        }

        /**
         * Method for syncing with stores
         * @param {String} action - the sync action (add, remove)
         * @param {String} key - the key for the item to be removed
         * @param {Object} item - the item to cache
         * @param {Number} ttl - the time to live for the item inside the cache
         * @private
         */
        function _syncStores(action, key, item, ttl) {
            if (!this.nostore) {
                for (var i = 0; i < this.stores.length; i++) {
                    if (this.stores[i][action]) {
                        this.stores[i][action](key, item, ttl);
                    }
                    else if (this.stores[i].save) {
                        this.stores[i].save({
                            items: this.cache
                        });
                    }
                }
            }
        }

        /**
         * Method for rejecting the promise
         * @param {String} key - the key for the item to be cached
         * @param {Object} item - the item to cache
         * @param {Number} ttl - the time to live for the item inside the cache
         * @param {Function} callback - optional callback to be called on item timeout
         * @returns {Boolean} indication whether the item had been added to the cache or not (since the cache is full)
         * @private
         */
        function _insert(key, item, ttl, callback) {
            var eviction;
            var timeout;

            if (0 === this.max || this.length < this.max || Cacher.MAX_STRATEGY.CLOSEST_TTL === this.maxStrategy) {
                eviction = (ttl && !isNaN(ttl) && 0 < ttl ? parseInt(ttl, 10) : this.ttl);

                this.cache[key] = {
                    item: item
                };

                this.length++;

                if (eviction) {
                    timeout = (new Date()).getTime() + eviction;
                    this.cache[key].timeout = timeout;
                    this.cache[key].ttl = eviction;
                }

                if ("function" === typeof callback) {
                    this.cache[key].callback = callback;
                }

                _syncStores.call(this, "set", key, item, ttl);

                if (eviction && (this.cache[key].callback || "function" === typeof this.ontimeout || "function" === typeof this.onkickout) || this.max && this.length > this.max) {
                    _evict.call(this, this.max && this.length > this.max);
                }

                return true;
            }
            else {
                return false;
            }
        }

        /**
         * Method for evicting expired items from the cache
         * @param {Boolean} kickoutClosestTTL - optional flag to force the removal of the item with the closest TTL
         * @returns {Number} The number of removed items from the cache
         * @private
         */
        function _evict(kickoutClosestTTL) {
            var callback;
            var item;
            var cbRes;
            var timeoutRes;
            var kickOut;
            var removed = 0;

            if (this.timer) {
                clearTimeout(this.timer);
            }

            if (this.length) {
                for (var key in this.cache) {
                    if (this.cache.hasOwnProperty(key) && this.cache[key].timeout) {
                        if (this.cache[key].timeout <= (new Date()).getTime()) {
                            item = this.cache[key].item;
                            callback = this.cache[key].callback;

                            if (callback) {
                                cbRes = callback(key, item);
                            }

                            if (this.ontimeout) {
                                timeoutRes = this.ontimeout(key, item);
                            }

                            // Now remove it
                            if (typeof cbRes === 'object') {
                                touch.call(this, key, cbRes.ttl || this.cache[key].ttl, cbRes.callback || callback);
                            } else if (typeof timeoutRes === 'object') {
                                touch.call(this, key, timeoutRes.ttl || this.cache[key].ttl, timeoutRes.callback || callback);
                            } else if (cbRes !== false && timeoutRes !== false) {
                                remove.call(this, key);
                                removed++;
                            } else if (!removed && kickoutClosestTTL) {
                                if (!kickOut) {
                                    kickOut = {
                                        key: key,
                                        timeout: this.cache[key].timeout
                                    };
                                }
                                else if (kickOut.timeout > this.cache[key].timeout) {
                                    kickOut.key = key;
                                    kickOut.timeout = this.cache[key].timeout;
                                }
                            }
                        }
                        else if (!removed && kickoutClosestTTL) {
                            if (!kickOut) {
                                kickOut = {
                                    key: key,
                                    timeout: this.cache[key].timeout
                                };
                            }
                            else if (kickOut.timeout > this.cache[key].timeout) {
                                kickOut.key = key;
                                kickOut.timeout = this.cache[key].timeout;
                            }
                        }
                    }
                }

                if (!removed && kickOut && this.cache[kickOut.key]) {
                    item = this.cache[kickOut.key].item;
                    callback = this.cache[kickOut.key].callback;

                    if (callback) {
                        callback(kickOut.key, item);
                    }

                    if (this.onkickout) {
                        this.onkickout(kickOut.key, item);
                    }

                    // Now remove it
                    remove.call(this, kickOut.key);
                    removed++;
                }
            }

            this.timer = setTimeout(_evict.bind(this), this.interval);

            return removed;
        }

        return {
            initialize: initialize,
            get: get,
            set: set,
            touch: touch,
            remove: remove,
            removeAll: removeAll
        };
    }());

    // attach properties to the exports object to define
    // the exported module properties.
    root.Cacher = root.Cacher || Cacher;
}))
;