/** * A BufferedStore maintains a sparsely populated map of pages corresponding to an extremely large * server-side dataset. * * **Note:** Buffered Stores are not available in the modern toolkit. Instead use * `Ext.data.virtual.Store`. * * Use a BufferedStore when the dataset size is so large that the database and network latency, * and client memory requirements preclude caching the entire dataset in a regular * {@link Ext.data.Store Store}. * * When using a BufferedStore *not all of the dataset is present in the client*. Only pages which * have been requested by the UI (usually a {@link Ext.grid.Panel GridPanel}) and surrounding pages * will be present. Retention of viewed pages in the BufferedStore after they have been scrolled * out of view is configurable. See {@link #leadingBufferZone}, {@link #trailingBufferZone} and * {@link #purgePageCount}. * * To use a BufferedStore, initiate the loading process by loading the first page. The number of * rows rendered are determined automatically, and the range of pages needed to keep the cache * primed for scrolling is requested and cached. * Example: * * myBufferedStore.loadPage(1); // Load page 1 * * A {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} is instantiated which will monitor * the scrolling in the grid, and refresh the view's rows from the page cache as needed. It will * also pull new data into the page cache when scrolling of the view draws upon data near either * end of the prefetched data. * * The margins which trigger view refreshing from the prefetched data are * {@link Ext.grid.plugin.BufferedRenderer#numFromEdge}, * {@link Ext.grid.plugin.BufferedRenderer#leadingBufferZone}, and * {@link Ext.grid.plugin.BufferedRenderer#trailingBufferZone}. * * The margins which trigger loading more data into the page cache are, {@link #leadingBufferZone} * and {@link #trailingBufferZone}. * * By default, only 5 pages of data (in addition to the pages which over the visible region) are * cached in the page cache, with old pages being evicted from the cache as the view moves down * through the dataset. This is controlled by the {@link #purgePageCount} setting. * * Setting this value to zero means that no pages are *ever* scrolled out of the page cache, and * that eventually the whole dataset may become present in the page cache. This is sometimes * desirable as long as datasets do not reach astronomical proportions. * * Selection state may be maintained across page boundaries by configuring the SelectionModel * not to discard records from its collection when those Records cycle out of the Store's primary * collection. This is done by configuring the SelectionModel like this: * * selModel: { * pruneRemoved: false * } * */Ext.define('Ext.data.BufferedStore', { extend: 'Ext.data.ProxyStore', alias: 'store.buffered', requires: [ 'Ext.data.PageMap', 'Ext.util.Filter', 'Ext.util.Sorter', 'Ext.util.Grouper' ], uses: [ 'Ext.util.SorterCollection', 'Ext.util.FilterCollection', 'Ext.util.GroupCollection' ], /** * @property {Boolean} isBufferedStore * `true` in this class to identify an object as an instantiated BufferedStore, or subclass * thereof. */ isBufferedStore: true, // For backward compatibility with user code. buffered: true, config: { data: 0, pageSize: 25, /** * @cfg remoteSort * @inheritdoc */ remoteSort: true, /** * @cfg remoteFilter * @inheritdoc */ remoteFilter: true, /** * @cfg sortOnLoad * @inheritdoc */ sortOnLoad: false, /** * @cfg {Number} purgePageCount * * The number of pages *in addition to twice the required buffered range* to keep * in the prefetch cache before purging least recently used records. * * For example, if the height of the view area and the configured * {@link #trailingBufferZone} and {@link #leadingBufferZone} require that there * are three pages in the cache, then a `purgePageCount` of 5 ensures that up to * 11 pages can be in the page cache any any one time. This is enough to allow * the user to scroll rapidly between different areas of the dataset without * evicting pages which are still needed. * * A value of 0 indicates to never purge the prefetched data. */ purgePageCount: 5, /** * @cfg {Number} trailingBufferZone * The number of extra records to keep cached on the trailing side of scrolling * buffer as scrolling proceeds. A larger number means fewer replenishments from * the server. */ trailingBufferZone: 25, /** * @cfg {Number} leadingBufferZone * The number of extra rows to keep cached on the leading side of scrolling * buffer as scrolling proceeds. A larger number means fewer replenishments from * the server. */ leadingBufferZone: 200, /** * @cfg {Number} defaultViewSize * The default view size to use until the * {@link #viewSize} has been configured. * @private */ defaultViewSize: 100, /** * @cfg {Number} viewSize The view size needed to fill the current view. Defaults * to the {@link #defaultViewSize}. * This will typically be set by the underlying view. * @private */ viewSize: 0, /** * @cfg {Boolean} trackRemoved * The {@link Ext.data.ProxyStore#cfg!trackRemoved trackRemoved} config is not * supported by buffered stores. */ trackRemoved: false }, /** * We are using applyData so that we can return nothing and prevent the `this.data` * property to be overridden. * @param {Array/Object} data */ applyData: function(data) { var dataCollection = this.data || (this.data = this.createDataCollection()); //<debug> if (data && data !== true) { Ext.raise('Cannot load a buffered store with local data - the store is a map ' + 'of remote data'); } //</debug> return dataCollection; }, applyProxy: function(proxy) { proxy = this.callParent([proxy]); // This store asks for pages. // If used with a MemoryProxy, it must work if (proxy && proxy.setEnablePaging) { proxy.setEnablePaging(true); } return proxy; }, applyAutoSort: function() { // Return undefined so that applier does not run. // BufferedStore/PageMap cannot sort. }, createFiltersCollection: function() { return new Ext.util.FilterCollection(); }, createSortersCollection: function() { return new Ext.util.SorterCollection(); }, //<debug> updateRemoteFilter: function(remoteFilter, oldRemoteFilter) { if (remoteFilter === false) { Ext.raise('Buffered stores are always remotely filtered.'); } this.callParent([remoteFilter, oldRemoteFilter]); }, updateRemoteSort: function(remoteSort, oldRemoteSort) { if (remoteSort === false) { Ext.raise('Buffered stores are always remotely sorted.'); } this.callParent([remoteSort, oldRemoteSort]); }, updateTrackRemoved: function(value) { if (value !== false) { Ext.raise('Cannot use trackRemoved with a buffered store.'); } this.callParent(arguments); }, //</debug> updateGroupField: function(field) { this.group(field); }, getGrouper: function() { return this.grouper; }, isGrouped: function() { return !!this.grouper; }, createDataCollection: function() { var me = this, result = new Ext.data.PageMap({ store: me, rootProperty: 'data', pageSize: me.getPageSize(), maxSize: me.getPurgePageCount(), listeners: { // Whenever PageMap gets cleared, it means we re no longer interested in // any outstanding page prefetches, so cancel them all clear: me.onPageMapClear, scope: me } }); // Allow view to veto prune if the old page is still in use by the view me.relayEvents(result, ['beforepageremove', 'pageadd', 'pageremove']); me.pageRequests = {}; return result; }, //<debug> add: function() { Ext.raise('add method may not be called on a buffered store - the store is a map ' + 'of remote data'); }, insert: function() { Ext.raise('insert method may not be called on a buffered store - the store is a map ' + 'of remote data'); }, //</debug> removeAll: function(silent) { var me = this, data = me.getData(); if (data) { if (silent) { me.suspendEvent('clear'); } data.clear(); if (silent) { me.resumeEvent('clear'); } } }, flushLoad: function() { var me = this, options = me.pendingLoadOptions; // If it gets called programmatically, the listener will need cancelling me.clearLoadTask(); if (!options) { return; } // Buffered stores, a load operation means kick off a clean load from page 1 // unless it's specified to preserve the options if (!options.preserveOnFlush) { me.getData().clear(); options.page = 1; options.start = 0; options.limit = me.getViewSize() || me.getDefaultViewSize(); } // If we're prefetching, the arguments on the callback for getting the range is different // So we indicate that we need to fire a special "load" style callback options.loadCallback = options.callback; // options might be chained, with callback on a prototype; delete won't clear it. options.callback = null; return me.loadToPrefetch(options); }, reload: function(options) { var me = this, data = me.getData(), // If we don't have a known totalCount, use a huge value lastTotal = Number.MAX_VALUE, startIdx, endIdx, startPage, endPage, i, waitForReload, bufferZone, records; if (!options) { options = {}; } // Prevent re-entering the load process if we are already in a wait state for a batch // of pages. if (me.loading || me.fireEvent('beforeload', me, options) === false) { return; } waitForReload = function() { var newCount = me.totalCount, oldRequestSize = endIdx - startIdx; // If the dataset has now shrunk leaving the calculated request zone unavailable, // re-evaluate the request zone. Start as close to the end as possible. if (endIdx >= newCount) { endIdx = newCount - 1; startIdx = Math.max(endIdx - oldRequestSize, 0); } if (me.rangeCached(startIdx, endIdx, false)) { me.loadCount = (me.loadCount || 0) + 1; me.loading = false; data.un('pageadd', waitForReload); records = data.getRange(startIdx, endIdx); me.fireEvent('refresh', me); me.fireEvent('load', me, records, true); } }; bufferZone = Math.ceil((me.getLeadingBufferZone() + me.getTrailingBufferZone()) / 2); // Decide what reload means. // If the View was configured preserveScrollOnReload, then it will // inject that setting here. This means that reload means // load the last requested range. if (me.lastRequestStart && me.preserveScrollOnReload) { startIdx = me.lastRequestStart; endIdx = me.lastRequestEnd; lastTotal = me.getTotalCount(); } // Otherwise, reload means start from page 1 else { startIdx = options.start || 0; endIdx = startIdx + (options.count || me.getPageSize()) - 1; } // Clear page cache data.clear(true); // So that prefetchPage does not consider the store to be fully loaded if the local count // is equal to the total count delete me.totalCount; // Calculate a page range which encompasses the Store's loaded range plus both buffer zones startIdx = Math.max(startIdx - bufferZone, 0); endIdx = Math.min(endIdx + bufferZone, lastTotal); // We must wait for a slightly wider range to be cached. // This is to allow grouping features to peek at the two surrounding records // when rendering a *range* of records to see whether the start of the range // really is a group start and the end of the range really is a group end. startIdx = startIdx === 0 ? 0 : startIdx - 1; endIdx = endIdx === lastTotal ? endIdx : endIdx + 1; startPage = me.getPageFromRecordIndex(startIdx); endPage = me.getPageFromRecordIndex(endIdx); me.loading = true; options.waitForReload = waitForReload; // Wait for the requested range to become available in the page map // Load the range as soon as the whole range is available data.on('pageadd', waitForReload); // Recache the page range which encapsulates our visible records for (i = startPage; i <= endPage; i++) { me.prefetchPage(i, options); } }, filter: function() { //<debug> if (!this.getRemoteFilter()) { Ext.raise('Local filtering may not be used on a buffered store - the store is a map ' + 'of remote data'); } //</debug> // Remote filtering forces a load. load clears the store's contents. this.callParent(arguments); }, filterBy: function(fn, scope) { //<debug> Ext.raise('Local filtering may not be used on a buffered store - the store is a map ' + 'of remote data'); //</debug> }, loadData: function(data, append) { //<debug> Ext.raise('LoadData may not be used on a buffered store - the store is a map ' + 'of remote data'); //</debug> }, loadPage: function(page, options) { var me = this; options = options || {}; options.page = me.currentPage = page; options.start = (page - 1) * me.getPageSize(); options.limit = me.getViewSize() || me.getDefaultViewSize(); options.loadCallback = options.callback; // options might be chained, with callback on a prototype; delete won't clear it. options.callback = null; // Since we're deferring these to flushLoad, we need to preserve the options. // Otherwise, they will be reset. options.preserveOnFlush = true; return me.load(options); }, clearData: function(isLoad) { var me = this, data = me.getData(); if (data) { data.clear(); } }, /** * @private * A BufferedStore always reports that it contains the full dataset. * The number of records that happen to be cached at any one time is never useful. */ getCount: function() { return this.totalCount || 0; }, getRange: function(start, end, options) { var me = this, maxIndex = me.totalCount - 1, lastRequestStart = me.lastRequestStart, result = [], data = me.getData(), pageAddHandler, requiredStart, requiredEnd, requiredStartPage, requiredEndPage; options = Ext.apply({ prefetchStart: start, prefetchEnd: end }, options); // Sanity check end point to be within dataset range end = (end >= me.totalCount) ? maxIndex : end; // If this is being called in the default manner, to fetch data // for rendering, then we must wait for a slightly wider range to be cached. // This is to allow grouping features to peek at the two surrounding records // when rendering a *range* of records to see whether the start of the range // really is a group start and the end of the range really is a group end. if (options.forRender !== false) { requiredStart = start === 0 ? 0 : start - 1; requiredEnd = end === maxIndex ? end : end + 1; } else { requiredStart = start; requiredEnd = end; } // Keep track of range we are being asked for so we can track direction of movement // through the dataset me.lastRequestStart = start; me.lastRequestEnd = end; // If data request can be satisfied from the page cache if (me.rangeCached(start, end, options.forRender)) { me.onRangeAvailable(options); result = data.getRange(start, end + 1); } // At least some of the requested range needs loading from server else { // Private event used by the LoadMask class to perform masking when the range // required for rendering is not found in the cache me.fireEvent('cachemiss', me, start, end); requiredStartPage = me.getPageFromRecordIndex(requiredStart); requiredEndPage = me.getPageFromRecordIndex(requiredEnd); // Add a pageadd listener, and as soon as the requested range is loaded, call // onRangeAvailable to call the callback. pageAddHandler = function(pageMap, page, records) { if (page >= requiredStartPage && page <= requiredEndPage && me.rangeCached(start, end)) { // Private event used by the LoadMask class to unmask when the range // required for rendering has been loaded into the cache me.fireEvent('cachefilled', me, start, end); data.un('pageadd', pageAddHandler); me.onRangeAvailable(options); } }; data.on('pageadd', pageAddHandler); // Prioritize the request for the *exact range that the UI is asking for*. // When a page request is in flight, it will not be requested again by checking // the me.pageRequests hash, so the request after this will only request the // *remaining* unrequested pages. me.prefetchRange(start, end); } // Load the pages around the requested range required by the leadingBufferZone // and trailingBufferZone. me.primeCache(start, end, start < lastRequestStart ? -1 : 1); return result; }, /** * Get the Record with the specified id. * * This method is not affected by filtering, lookup will be performed from all records * inside the store, filtered or not. * * @param {Mixed} id The id of the Record to find. * @return {Ext.data.Model} The Record with the passed id. Returns null if not found. */ getById: function(id) { var result = this.data.findBy(function(record) { return record.getId() === id; }); return result; }, /** * @method getAt * @inheritdoc */ getAt: function(index) { var data = this.getData(); if (data.hasRange(index, index)) { return data.getAt(index); } }, /** * @private * Get the Record with the specified internalId. * * This method is not effected by filtering, lookup will be performed from all records * inside the store, filtered or not. * * @param {Mixed} internalId The id of the Record to find. * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found. */ getByInternalId: function(internalId) { return this.data.getByInternalId(internalId); }, /** * @method contains * @inheritdoc */ contains: function(record) { return this.indexOf(record) > -1; }, /** * Get the index of the record within the store. * * When store is filtered, records outside of filter will not be found. * * @param {Ext.data.Model} record The Ext.data.Model object to find. * @return {Number} The index of the passed Record. Returns -1 if not found. */ indexOf: function(record) { return this.getData().indexOf(record); }, /** * Get the index within the store of the Record with the passed id. * * Like #indexOf, this method is effected by filtering. * * @param {String} id The id of the Record to find. * @return {Number} The index of the Record. Returns -1 if not found. */ indexOfId: function(id) { return this.indexOf(this.getById(id)); }, group: function(grouper, direction) { var me = this, oldGrouper; if (grouper && typeof grouper === 'string') { oldGrouper = me.grouper; if (oldGrouper && direction !== undefined) { oldGrouper.setDirection(direction); } else { me.grouper = new Ext.util.Grouper({ property: grouper, direction: direction || 'ASC', root: 'data' }); } } else { me.grouper = grouper ? me.getSorters().decodeSorter(grouper, Ext.util.Grouper) : null; } me.getData().clear(); me.loadPage(1, { callback: function() { me.fireEvent('groupchange', me, me.getGrouper()); } }); }, /** * Determines the page from a record index * @param {Number} index The record index * @return {Number} The page the record belongs to */ getPageFromRecordIndex: function(index) { return Math.floor(index / this.getPageSize()) + 1; }, calculatePageCacheSize: function(rangeSizeRequested) { var me = this, purgePageCount = me.getPurgePageCount(); // Calculate the number of pages that the cache will keep before purging as follows: // TWO full rendering zones (in case of rapid teleporting by dragging the scroller) // plus configured purgePageCount. // Ensure we never reduce the count. It always uses the largest requested block // as the basis for the calculated size. /* eslint-disable-next-line max-len */ return purgePageCount ? Math.max(me.getData().getMaxSize() || 0, Math.ceil((rangeSizeRequested + me.getTrailingBufferZone() + me.getLeadingBufferZone()) / me.getPageSize()) * 2 + purgePageCount) : 0; }, /* eslint-disable max-len */ loadToPrefetch: function(options) { var me = this, prefetchOptions = options, i, records, dataSetSize, // Get the requested record index range in the dataset startIdx = options.start, endIdx = options.start + options.limit - 1, rangeSizeRequested = (me.getViewSize() || options.limit), // The end index to load into the store's live record collection loadEndIdx = Math.min(endIdx, options.start + rangeSizeRequested - 1), // Calculate a page range which encompasses the requested range plus both buffer zones. // The endPage will be adjusted to be in the dataset size range as soon as the first // data block returns. startPage = me.getPageFromRecordIndex(Math.max(startIdx - me.getTrailingBufferZone(), 0)), endPage = me.getPageFromRecordIndex(endIdx + me.getLeadingBufferZone()), data = me.getData(), callbackFn = function() { // See comments in load() for why we need this. records = records || []; if (options.loadCallback) { options.loadCallback.call(options.scope || me, records, operation, true); } if (options.callback) { options.callback.call(options.scope || me, records, startIdx || 0, endIdx || 0, options); } }, fireEventsFn = function() { me.loadCount = (me.loadCount || 0) + 1; me.fireEvent('datachanged', me); me.fireEvent('refresh', me); me.fireEvent('load', me, records, true); }, // Wait for the viewable range to be available. waitForRequestedRange = function() { if (me.rangeCached(startIdx, loadEndIdx)) { me.loading = false; records = data.getRange(startIdx, loadEndIdx + 1); data.un('pageadd', waitForRequestedRange); // If there is a listener for guaranteedrange then fire that event if (me.hasListeners.guaranteedrange) { me.guaranteeRange(startIdx, loadEndIdx, options.callback, options.scope); } callbackFn(); fireEventsFn(); } }, operation; //<debug> if (isNaN(me.pageSize) || !me.pageSize) { Ext.raise('Buffered store configured without a pageSize', me); } //</debug> // Ensure that the purgePageCount allows enough pages to be kept cached to cover the // requested range. If the pageSize is very small we might need a lot of pages. data.setMaxSize(me.calculatePageCacheSize(rangeSizeRequested)); if (me.fireEvent('beforeload', me, options) !== false) { // So that prefetchPage does not consider the store to be fully loaded if the local // count is equal to the total count delete me.totalCount; me.loading = true; // Any configured callback is handled in waitForRequestedRange above. // It should not be processed by onProxyPrefetch. if (options.callback) { prefetchOptions = Ext.apply({}, options); delete prefetchOptions.callback; } // Load the first page in the range, which will give us the initial total count. // Once it is loaded, go ahead and prefetch any subsequent pages, if necessary. // The prefetchPage has a check to prevent us loading more than the totalCount, // so we don't want to blindly load up <n> pages where it isn't required. me.on('prefetch', function(store, records, successful, op) { // Capture operation here so it can be used in the loadCallback above operation = op; if (successful) { // If there is data in the dataset, we can go ahead and add the pageadd // listener which waits for the visible range and we can also issue the // requests to fill the surrounding buffer zones. if ((dataSetSize = me.getTotalCount())) { // Wait for the requested range to become available in the page map data.on('pageadd', waitForRequestedRange); // As soon as we have the size of the dataset, ensure we are not waiting // for more than can ever arrive, loadEndIdx = Math.min(loadEndIdx, dataSetSize - 1); // And make sure we never ask for pages beyond the end of the dataset. endPage = me.getPageFromRecordIndex(Math.min(loadEndIdx + me.getLeadingBufferZone(), dataSetSize - 1)); for (i = startPage + 1; i <= endPage; ++i) { me.prefetchPage(i, prefetchOptions); } } else { callbackFn(); fireEventsFn(); } } // Unsuccessful prefetch: fire a load event with success false. else { me.loading = false; callbackFn(); me.fireEvent('load', me, records, false); } }, null, { single: true }); me.prefetchPage(startPage, prefetchOptions); } }, /* eslint-enable max-len */ // Buffering /** * Prefetches data into the store using its configured {@link #proxy}. * @param {Object} options (Optional) config object, passed into the * Ext.data.operation.Operation object before loading. * See {@link #method-load} */ prefetch: function(options) { var me = this, pageSize = me.getPageSize(), data = me.getData(), operation, existingPageRequest; // Check pageSize has not been tampered with. That would break page caching if (pageSize) { if (me.lastPageSize && pageSize !== me.lastPageSize) { Ext.raise("pageSize cannot be dynamically altered"); } if (!data.getPageSize()) { data.setPageSize(pageSize); } } // Allow first prefetch call to imply the required page size. else { me.pageSize = data.setPageSize(pageSize = options.limit); } // So that we can check for tampering next time through me.lastPageSize = pageSize; // Always get whole pages. if (!options.page) { options.page = me.getPageFromRecordIndex(options.start); options.start = (options.page - 1) * pageSize; options.limit = Math.ceil(options.limit / pageSize) * pageSize; } // Currently not requesting this page, or the request was for the last // generation of the data cache (clearing it changes generations) // then request it... existingPageRequest = me.pageRequests[options.page]; if (!existingPageRequest || existingPageRequest.getOperation().pageMapGeneration !== data.pageMapGeneration) { // Copy options into a new object so as not to mutate passed in objects options = Ext.apply({ action: 'read', filters: me.getFilters().items, sorters: me.getSorters().items, grouper: me.getGrouper(), internalCallback: me.onProxyPrefetch, internalScope: me }, options); operation = me.createOperation('read', options); // Generation # of the page map to which the requested records belong. // If page map is cleared while this request is in flight, the pageMapGeneration // will increment and the payload will be rejected operation.pageMapGeneration = data.pageMapGeneration; if (me.fireEvent('beforeprefetch', me, operation) !== false) { me.pageRequests[options.page] = operation.execute(); if (me.getProxy().isSynchronous) { delete me.pageRequests[options.page]; } } } return me; }, /** * @private * Cancels all pending prefetch requests. * * This is called when the page map is cleared. * * Any requests which still make it through will be for the previous pageMapGeneration * (pageMapGeneration is incremented upon clear), and so will be rejected upon arrival. */ onPageMapClear: function() { var me = this, loadingFlag = me.wasLoading, reqs = me.pageRequests, data = me.getData(), page; // If any requests return, we no longer respond to them. data.clearListeners(); // replace the listeners we need. data.on('clear', me.onPageMapClear, me); me.relayEvents(data, ['beforepageremove', 'pageadd', 'pageremove']); // If the page cache gets cleared it's because a full reload is in progress. // Setting the loading flag prevents linked Views from displaying the empty text // during a load... we don't know whether their dataset is empty or not. me.loading = true; me.totalCount = 0; // Abort all outstanding requests. // onProxyPrefetch will reject them as being for the previous data generation // anyway, if they do return. // because of the pageMapGeneration mismatch. for (page in reqs) { if (reqs.hasOwnProperty(page)) { reqs[page].getOperation().abort(); } } // This will update any views. me.fireEvent('clear', me); // Restore loading flag. The beforeload event could still veto the process. // The flag does not get set for real until we pass the beforeload event. me.loading = loadingFlag; }, /** * Prefetches a page of data. * @param {Number} page The page to prefetch * @param {Object} options (Optional) config object, passed into the * Ext.data.operation.Operation object before loading. * See {@link #method-load} */ prefetchPage: function(page, options) { var me = this, pageSize = me.getPageSize(), start = (page - 1) * pageSize, total = me.totalCount; // No more data to prefetch. if (total !== undefined && me.data.getCount() === total) { return; } // Copy options into a new object so as not to mutate passed in objects me.prefetch(Ext.applyIf({ page: page, start: start, limit: pageSize }, options)); }, /** * Called after the configured proxy completes a prefetch operation. * @private * @param {Ext.data.operation.Operation} operation The operation that completed */ onProxyPrefetch: function(operation) { if (this.destroying || this.destroyed) { return; } /* eslint-disable-next-line vars-on-top */ var me = this, resultSet = operation.getResultSet(), records = operation.getRecords(), successful = operation.wasSuccessful(), page = operation.getPage(), waitForReload = operation.waitForReload, oldTotal = me.totalCount, requests = me.pageRequests, key, op; // Only cache the data if the operation was invoked for the current pageMapGeneration. // If the pageMapGeneration has changed since the request was fired off, it will have been // cancelled. if (operation.pageMapGeneration === me.getData().pageMapGeneration) { if (resultSet) { me.totalCount = resultSet.getTotal(); if (me.totalCount !== oldTotal) { me.fireEvent('totalcountchange', me.totalCount); } } // Remove the loaded page from the outstanding pages hash if (page !== undefined) { delete me.pageRequests[page]; } // Prefetch is broadcast before the page is cached me.loading = false; me.fireEvent('prefetch', me, records, successful, operation); // Add the page into the page map. // pageadd event may trigger the onRangeAvailable if (successful) { if (me.totalCount === 0) { if (waitForReload) { for (key in requests) { op = requests[key].getOperation(); // Created in the same batch, clear the waitForReload so this // won't be run again if (op.waitForReload === waitForReload) { delete op.waitForReload; } } me.getData().un('pageadd', waitForReload); me.fireEvent('refresh', me); me.fireEvent('load', me, [], true); } } else { me.cachePage(records, operation.getPage()); } } // this is a callback that would have been passed to the 'read' function and is optional Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]); } }, /** * Caches the records in the prefetch and stripes them with their server-side * index. * @private * @param {Ext.data.Model[]} records The records to cache * @param {Ext.data.operation.Operation} page The associated operation */ cachePage: function(records, page) { var me = this, len = records.length, i; if (!Ext.isDefined(me.totalCount)) { me.totalCount = records.length; me.fireEvent('totalcountchange', me.totalCount); } // Add the fetched page into the pageCache for (i = 0; i < len; i++) { records[i].join(me); } me.getData().addPage(page, records); }, /** * Determines if the passed range is available in the page cache. * @private * @param {Number} start The start index * @param {Number} end The end index in the range * @param {Boolean} [forRender] (private) Passed by the BufferedRenderer to * indicate that it's going to need extra rows to peek at to determine * group start/end status for the rendered block. */ rangeCached: function(start, end, forRender) { var requiredStart = start, requiredEnd = end; // If this is for getting data to render, we must wait for a slightly wider range // to be cached. This is to allow grouping features to peek at the two surrounding records // when rendering a *range* of records to see whether the start of the range // really is a group start and the end of the range really is a group end. if (forRender !== false) { requiredStart = start === 0 ? 0 : start - 1, requiredEnd = end === this.totalCount - 1 ? end : end + 1; } return this.getData().hasRange(requiredStart, requiredEnd); }, /** * Determines if the passed page is available in the page cache. * @private * @param {Number} page The page to find in the page cache. */ pageCached: function(page) { return this.getData().hasPage(page); }, /** * Determines if a request for a page is currently running * @private * @param {Number} page The page to check for */ pagePending: function(page) { return !!this.pageRequests[page]; }, /** * Determines if the passed range is available in the page cache. * @private * @deprecated 4.1.0 use {@link #rangeCached} instead * @param {Number} start The start index * @param {Number} end The end index in the range * @return {Boolean} */ rangeSatisfied: function(start, end) { return this.rangeCached(start, end); }, /** * Handles the availability of a requested range that was not previously available * @private */ onRangeAvailable: function(options) { var me = this, totalCount = me.getTotalCount(), start = options.prefetchStart, end = (options.prefetchEnd > totalCount - 1) ? totalCount - 1 : options.prefetchEnd, range; end = Math.max(0, end); //<debug> if (start > end) { Ext.log({ level: 'warn', msg: 'Start (' + start + ') was greater than end (' + end + ') for the range of records requested (' + start + '-' + options.prefetchEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '') }); } //</debug> range = me.getData().getRange(start, end + 1); if (options.fireEvent !== false) { me.fireEvent('guaranteedrange', range, start, end, options); } if (options.callback) { options.callback.call(options.scope || me, range, start, end, options); } }, /** * Guarantee a specific range, this will load the store with a range (that * must be the `pageSize` or smaller) and take care of any loading that may * be necessary. * @param {Number} start The starting index. Defaults to zero. * @param {Number} end The ending index. Defaults to the last record. The end index * **is included**. * @param {Function} callback Function to call with the results * @param {Object} [scope] The `this` pointer for the `callback`. * @param {Object} [options] (private) * @deprecated 4.2 Use {@link #getRange} */ guaranteeRange: function(start, end, callback, scope, options) { options = Ext.apply({ callback: callback, scope: scope }, options); this.getRange(start, end + 1, options); }, /** * Ensures that the specified range of rows is present in the cache. * * Converts the row range to a page range and then only load pages which are not already * present in the page cache. */ prefetchRange: function(start, end) { var me = this, startPage, endPage, page, data = me.getData(); if (!me.rangeCached(start, end)) { startPage = me.getPageFromRecordIndex(start); endPage = me.getPageFromRecordIndex(end); // Ensure that the page cache's max size is correct. // Our purgePageCount is the number of additional pages *outside of the required range* // which may be kept in the cache. A purgePageCount of zero means unlimited. data.setMaxSize(me.calculatePageCacheSize(end - start + 1)); // We have the range, but ensure that we have a "buffer" of pages around it. for (page = startPage; page <= endPage; page++) { if (!me.pageCached(page)) { me.prefetchPage(page); } } } }, primeCache: function(start, end, direction) { var me = this, leadingBufferZone = me.getLeadingBufferZone(), trailingBufferZone = me.getTrailingBufferZone(), pageSize = me.getPageSize(), totalCount = me.totalCount; // Scrolling up if (direction === -1) { start = Math.max(start - leadingBufferZone, 0); end = Math.min(end + trailingBufferZone, totalCount - 1); } // Scrolling down else if (direction === 1) { start = Math.max(Math.min(start - trailingBufferZone, totalCount - pageSize), 0); end = Math.min(end + leadingBufferZone, totalCount - 1); } // Teleporting else { /* eslint-disable max-len */ start = Math.min(Math.max(Math.floor(start - ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - me.pageSize); end = Math.min(Math.max(Math.ceil(end + ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - 1); /* eslint-enable max-len */ } me.prefetchRange(start, end); }, sort: function(field, direction, mode) { if (arguments.length === 0) { this.clearAndLoad(); } else { this.getSorters().addSort(field, direction, mode); } }, onSorterEndUpdate: function() { var me = this, sorters = me.getSorters().getRange(); // Only load or sort if there are sorters if (sorters.length) { me.fireEvent('beforesort', me, sorters); me.clearAndLoad({ callback: function() { me.fireEvent('sort', me, sorters); } }); } else { // Sort event must fire when sorters collection is updated to empty. me.fireEvent('sort', me, sorters); } }, clearAndLoad: function(options) { var me = this; me.clearing = true; me.getData().clear(); me.clearing = false; me.loadPage(1, options); }, privates: { isLast: function(record) { return this.indexOf(record) === this.getTotalCount() - 1; }, isMoving: function() { return false; } }});