    /**
     * @class Ext.data.PageMap
     * @extends Ext.util.LruCache
     * Private class for use by only Store when configured `buffered: true`.
     * @private
     */
    Ext.define('Ext.data.PageMap', {
        extend: 'Ext.util.LruCache',

        // Maintain a generation counter, so that the Store can reject incoming pages destined for the previous generation
        clear: function(initial) {
            var me = this;
            me.pageMapGeneration = (me.pageMapGeneration || 0) + 1;
            me.callParent(arguments);
        },

        forEach: function(fn, scope) {
            var me = this,
                pageNumbers = Ext.Object.getKeys(me.map),
                pageCount = pageNumbers.length,
                i, j,
                pageNumber,
                page,
                pageSize;

            for (i = 0; i < pageCount; i++) {
                pageNumbers[i] = Number(pageNumbers[i]);
            }
            Ext.Array.sort(pageNumbers);
            scope = scope || me;
            for (i = 0; i < pageCount; i++) {
                pageNumber = pageNumbers[i];
                page = me.getPage(pageNumber);
                pageSize = page.length;
                for (j = 0; j < pageSize; j++) {
                    if (fn.call(scope, page[j], (pageNumber - 1) * me.pageSize + j) === false) {
                        return;
                    }
                }
            }
        },

        /**
        * Returns the first record in this page map which elicits a true return value from the
        * passed selection function.
        *
        * **IMPORTANT
        * This can ONLY find records which happen to be cached in the page cache. This will be parts of the dataset around the currently
        * visible zone, or recently visited zones if the pages have not yet been purged from the cache.
        *
        * This CAN NOT find records which have not been loaded into the cache.**
        *
        * If full client side searching is required, do not use a buffered store, instead use a regular, fully loaded store and
        * use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} plugin to minimize DOM footprint.
        * @param {Function} fn The selection function to execute for each item.
        *  @param {Mixed} fn.rec The record.
        *  @param {Mixed} fn.index The index in the total dataset of the record.
        * @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to this PageMap.
        * @return {Object} The first record in this page map which returned true from the selection
        * function, or null if none was found.
        */
        findBy: function(fn, scope) {
            var me = this,
                result = null;

            scope = scope || me;
            me.forEach(function(rec, index) {
                if (fn.call(scope, rec, index)) {
                    result = rec;
                    return false;
                }
            });
            return result;
        },

        /**
        * Returns the index *in the whole dataset* of the first record in this page map which elicits a true return value from the
        * passed selection function.
        *
        * **IMPORTANT
        * This can ONLY find records which happen to be cached in the page cache. This will be parts of the dataset around the currently
        * visible zone, or recently visited zones if the pages have not yet been purged from the cache.
        *
        * This CAN NOT find records which have not been loaded into the cache.**
        *
        * If full client side searching is required, do not use a buffered store, instead use a regular, fully loaded store and
        * use the {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} plugin to minimize DOM footprint.
        * @param {Function} fn The selection function to execute for each item.
        *  @param {Mixed} fn.rec The record.
        *  @param {Mixed} fn.index The index in the total dataset of the record.
        * @param {Object} [scope] The scope (`this` reference) in which the function is executed. Defaults to this PageMap.
        * @return {Number} The index first record in this page map which returned true from the selection
        * function, or -1 if none was found.
        */
        findIndexBy: function(fn, scope) {
            var me = this,
                result = -1;

            scope = scope || me;
            me.forEach(function(rec, index) {
                if (fn.call(scope, rec)) {
                    result = index;
                    return false;
                }
            });
            return result;
        },

        getPageFromRecordIndex: function() {
            return Ext.data.Store.prototype.getPageFromRecordIndex.apply(this, arguments);
        },

        addAll: function(records) {
            //<debug>
            if (this.getCount()) {
                Ext.Error.raise('Cannot addAll to a non-empty PageMap');
            }
            //</debug>
            this.addPage(1, records);
        },

        addPage: function(pageNumber, records) {
            var me = this,
                lastPage = pageNumber + Math.floor((records.length - 1) / me.pageSize),
                startIdx,
                page;

            // Account for being handed a block of records spanning several pages.
            // This can happen when loading from a MemoryProxy before a viewSize has been determined.
            for (startIdx = 0; pageNumber <= lastPage; pageNumber++, startIdx += me.pageSize) {
                page = Ext.Array.slice(records, startIdx, startIdx + me.pageSize);
                me.add(pageNumber, page);
                me.fireEvent('pageAdded', pageNumber, page);
            }
        },

        getCount: function() {
            var result = this.callParent();
            if (result) {
                result = (result - 1) * this.pageSize + this.last.value.length;
            }
            return result;
        },

        indexOf: function(record) {
            return record ? record.index : -1;
        },

        insert: function() {
            //<debug>
            Ext.Error.raise('insert operation not suppported into buffered Store');
            //</debug>
        },

        remove: function() {
            //<debug>
            Ext.Error.raise('remove operation not suppported from buffered Store');
            //</debug>
        },

        removeAt: function() {
            //<debug>
            Ext.Error.raise('removeAt operation not suppported from buffered Store');
            //</debug>
        },

        getPage: function(pageNumber) {
            return this.get(pageNumber);
        },

        hasRange: function(start, end) {
            var pageNumber = this.getPageFromRecordIndex(start),
                endPageNumber = this.getPageFromRecordIndex(end);

            for (; pageNumber <= endPageNumber; pageNumber++) {
                if (!this.hasPage(pageNumber)) {
                    return false;
                }
            }
            return true;
        },

        hasPage: function(pageNumber) {
            // We must use this.get to trigger an access so that the page which is checked for presence is not eligible for pruning
            return !!this.get(pageNumber);
        },

        getAt: function(index) {
            return this.getRange(index, index)[0];
        },

        getRange: function(start, end) {
            if (!this.hasRange(start, end)) {
                Ext.Error.raise('PageMap asked for range which it does not have');
            }
            var me = this,
                startPageNumber = me.getPageFromRecordIndex(start),
                endPageNumber = me.getPageFromRecordIndex(end),
                dataStart = (startPageNumber - 1) * me.pageSize,
                dataEnd = (endPageNumber * me.pageSize) - 1,
                pageNumber = startPageNumber,
                result = [],
                sliceBegin, sliceEnd, doSlice,
                i = 0, len;

            for (; pageNumber <= endPageNumber; pageNumber++) {

                // First and last pages will need slicing to cut into the actual wanted records
                if (pageNumber == startPageNumber) {
                    sliceBegin = start - dataStart;
                    doSlice = true;
                } else {
                    sliceBegin = 0;
                    doSlice = false;
                }
                if (pageNumber == endPageNumber) {
                    sliceEnd = me.pageSize - (dataEnd - end);
                    doSlice = true;
                }

                // First and last pages will need slicing
                if (doSlice) {
                    Ext.Array.push(result, Ext.Array.slice(me.getPage(pageNumber), sliceBegin, sliceEnd));
                } else {
                    Ext.Array.push(result, me.getPage(pageNumber));
                }
            }

            // Inject the dataset ordinal position into the record as the index
            for (len = result.length; i < len; i++) {
                result[i].index = start++;
            }
            return result;
        }
    });