LruCache.js 6.56 KB
/**
 * @private
 * @class Ext.util.LruCache
 * @extend Ext.util.HashMap
 * A linked {@link Ext.util.HashMap HashMap} implementation which maintains most recently accessed
 * items at the end of the list, and purges the cache down to the most recently accessed {@link #maxSize} items
 * upon add.
 */
Ext.define('Ext.util.LruCache', {
    extend: 'Ext.util.HashMap',

    /**
     * @cfg {Number} maxSize The maximum size the cache is allowed to grow to before further additions cause
     * removal of the least recently used entry.
     */

    constructor: function(config) {
        Ext.apply(this, config);
        this.callParent([config]);
    },

    /*
     * @inheritdoc
     */
    add: function(key, newValue) {
        var me = this,
            existingKey = me.findKey(newValue),
            entry;

        // "new" value is in the list.
        if (existingKey) {
            me.unlinkEntry(entry = me.map[existingKey]);
            entry.prev = me.last;
            entry.next = null;
        }
        // Genuinely new: create an entry for it.
        else {
            entry = {
                prev: me.last,
                next: null,
                key: key,
                value: newValue
            };
        }

        // If the list is not empty, update the last entry
        if (me.last) {
            me.last.next = entry;
        }
        // List is empty
        else {
            me.first = entry;
        }
        me.last = entry;
        me.callParent([key, entry]);
        me.prune();
        return newValue;
    },

    // @private
    insertBefore: function(key, newValue, sibling) {
        var me = this,
            existingKey,
            entry;

        // NOT an assignment.
        // If there is a following sibling
        if (sibling = this.map[this.findKey(sibling)]) {
            existingKey = me.findKey(newValue);

            // "new" value is in the list.
            if (existingKey) {
                me.unlinkEntry(entry = me.map[existingKey]);
            }
            // Genuinely new: create an entry for it.
            else {
                entry = {
                    prev: sibling.prev,
                    next: sibling,
                    key: key,
                    value: newValue
                };
            }

            if (sibling.prev) {
                entry.prev.next = entry;
            } else {
                me.first = entry;
            }
            entry.next = sibling;
            sibling.prev = entry;
            me.prune();
            return newValue;
        }
        // No following sibling, it's just an add.
        else {
            return me.add(key, newValue);
        }
    },

    /*
     * @inheritdoc
     */
    get: function(key) {
        var entry = this.map[key];
        if (entry) {

            // If it's not the end, move to end of list on get
            if (entry.next) {
                this.moveToEnd(entry);
            }
            return entry.value;
        }
    },

    /*
     * @private
     */
    removeAtKey: function(key) {
        this.unlinkEntry(this.map[key]);
        return this.callParent(arguments);
    },

    /*
     * @inheritdoc
     */
    clear: function(/* private */ initial) {
        this.first = this.last = null;
        return this.callParent(arguments);
    },

    // private. Only used by internal methods.
    unlinkEntry: function(entry) {
        // Stitch the list back up.
        if (entry) {
            if (entry.next) {
                entry.next.prev = entry.prev;
            } else {
                this.last = entry.prev;
            }
            if (entry.prev) {
                entry.prev.next = entry.next;
            } else {
                this.first = entry.next;
            }
            entry.prev = entry.next = null;
        }
    },

    // private. Only used by internal methods.
    moveToEnd: function(entry) {
        this.unlinkEntry(entry);

        // NOT an assignment.
        // If the list is not empty, update the last entry
        if (entry.prev = this.last) {
            this.last.next = entry;
        }
        // List is empty
        else {
            this.first = entry;
        }
        this.last = entry;
    },

    /*
     * @private
     */
    getArray: function(isKey) {
        var arr = [],
            entry = this.first;

        while (entry) {
            arr.push(isKey ? entry.key: entry.value);
            entry = entry.next;
        }
        return arr;
    },

    /**
     * Executes the specified function once for each item in the cache.
     * Returning false from the function will cease iteration.
     *
     * By default, iteration is from least recently used to most recent.
     *
     * The paramaters passed to the function are:
     * <div class="mdetail-params"><ul>
     * <li><b>key</b> : String<p class="sub-desc">The key of the item</p></li>
     * <li><b>value</b> : Number<p class="sub-desc">The value of the item</p></li>
     * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the hash</p></li>
     * </ul></div>
     * @param {Function} fn The function to execute.
     * @param {Object} scope The scope (<code>this</code> reference) to execute in. Defaults to this LruCache.
     * @param {Boolean} [reverse=false] Pass <code>true</code> to iterate the list in reverse (most recent first) order.
     * @return {Ext.util.LruCache} this
     */
    each: function(fn, scope, reverse) {
        var me = this,
            entry = reverse ? me.last : me.first,
            length = me.length;

        scope = scope || me;
        while (entry) {
            if (fn.call(scope, entry.key, entry.value, length) === false) {
                break;
            }
            entry = reverse ? entry.prev : entry.next;
        }
        return me;
    },

    /**
     * @private
     */
    findKey: function(value) {
        var key,
            map = this.map;

        for (key in map) {
            if (map.hasOwnProperty(key) && map[key].value === value) {
                return key;
            }
        }
        return undefined;
    },

    /**
     * Purge the least recently used entries if the maxSize has been exceeded.
     */
    prune: function() {
        var me = this,
            purgeCount = me.maxSize ? (me.length - me.maxSize) : 0;

        if (purgeCount > 0) {
            for (; me.first && purgeCount; purgeCount--) {
                me.removeAtKey(me.first.key);
            }
        }
    }

  /**
   * @method containsKey
   * @private
   */
  /**
   * @method contains
   * @private
   */
  /**
   * @method getKeys
   * @private
   */
  /**
   * @method getValues
   * @private
   */
});