]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/data/BufferedStore.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / data / BufferedStore.js
CommitLineData
6527f429
DM
1/**\r
2 * A BufferedStore maintains a sparsely populated map of pages corresponding to an extremely large server-side dataset.\r
3 *\r
4 * Use a BufferedStore when the dataset size is so large that the database and network latency, and client memory requirements\r
5 * preclude caching the entire dataset in a regular {@link Ext.data.Store Store}.\r
6 *\r
7 * When using a BufferedStore *not all of the dataset is present in the client*. Only pages which have been\r
8 * requested by the UI (usually a {@link Ext.grid.Panel GridPanel}) and surrounding pages will be present. Retention\r
9 * of viewed pages in the BufferedStore after they have been scrolled out of view is configurable. See {@link #leadingBufferZone},\r
10 * {@link #trailingBufferZone} and {@link #purgePageCount}.\r
11 *\r
12 * To use a BufferedStore, initiate the loading process by loading the first page. The number of rows rendered are\r
13 * determined automatically, and the range of pages needed to keep the cache primed for scrolling is\r
14 * requested and cached.\r
15 * Example:\r
16 *\r
17 * myBufferedStore.loadPage(1); // Load page 1\r
18 *\r
19 * A {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} is instantiated which will monitor the scrolling in the grid, and\r
20 * refresh the view's rows from the page cache as needed. It will also pull new data into the page\r
21 * cache when scrolling of the view draws upon data near either end of the prefetched data.\r
22 *\r
23 * The margins which trigger view refreshing from the prefetched data are {@link Ext.grid.plugin.BufferedRenderer#numFromEdge},\r
24 * {@link Ext.grid.plugin.BufferedRenderer#leadingBufferZone} and {@link Ext.grid.plugin.BufferedRenderer#trailingBufferZone}.\r
25 *\r
26 * The margins which trigger loading more data into the page cache are, {@link #leadingBufferZone} and\r
27 * {@link #trailingBufferZone}.\r
28 *\r
29 * By default, only 5 pages of data (in addition to the pages which over the visible region) are cached in the page cache,\r
30 * with old pages being evicted from the cache as the view moves down through the dataset. This is controlled by the\r
31 * {@link #purgePageCount} setting.\r
32 *\r
33 * Setting this value to zero means that no pages are *ever* scrolled out of the page cache, and\r
34 * that eventually the whole dataset may become present in the page cache. This is sometimes desirable\r
35 * as long as datasets do not reach astronomical proportions.\r
36 *\r
37 * Selection state may be maintained across page boundaries by configuring the SelectionModel not to discard\r
38 * records from its collection when those Records cycle out of the Store's primary collection. This is done\r
39 * by configuring the SelectionModel like this:\r
40 *\r
41 * selModel: {\r
42 * pruneRemoved: false\r
43 * }\r
44 *\r
45 */\r
46Ext.define('Ext.data.BufferedStore', {\r
47 extend: 'Ext.data.ProxyStore',\r
48\r
49 alias: 'store.buffered',\r
50\r
51 requires: [\r
52 'Ext.data.PageMap',\r
53 'Ext.util.Filter',\r
54 'Ext.util.Sorter',\r
55 'Ext.util.Grouper'\r
56 ],\r
57 \r
58 uses: [\r
59 'Ext.util.SorterCollection',\r
60 'Ext.util.FilterCollection',\r
61 'Ext.util.GroupCollection'\r
62 ],\r
63\r
64 /**\r
65 * @property {Boolean} isBufferedStore\r
66 * `true` in this class to identify an object as an instantiated BufferedStore, or subclass thereof.\r
67 */\r
68 isBufferedStore: true,\r
69\r
70 // For backward compatibility with user code.\r
71 buffered: true,\r
72\r
73 config: {\r
74 data: 0,\r
75 pageSize: 25,\r
76 remoteSort: true,\r
77 remoteFilter: true,\r
78 sortOnLoad: false,\r
79 /**\r
80 * @cfg {Number} purgePageCount\r
81 *\r
82 * The number of pages *in addition to twice the required buffered range* to keep in the prefetch cache before purging least recently used records.\r
83 *\r
84 * For example, if the height of the view area and the configured {@link #trailingBufferZone} and {@link #leadingBufferZone} require that there\r
85 * 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\r
86 * to allow the user to scroll rapidly between different areas of the dataset without evicting pages which are still needed.\r
87 *\r
88 * A value of 0 indicates to never purge the prefetched data.\r
89 */\r
90 purgePageCount: 5,\r
91\r
92 /**\r
93 * @cfg {Number} trailingBufferZone\r
94 * The number of extra records to keep cached on the trailing side of scrolling buffer\r
95 * as scrolling proceeds. A larger number means fewer replenishments from the server.\r
96 */\r
97 trailingBufferZone: 25,\r
98\r
99 /**\r
100 * @cfg {Number} leadingBufferZone\r
101 * The number of extra rows to keep cached on the leading side of scrolling buffer\r
102 * as scrolling proceeds. A larger number means fewer replenishments from the server.\r
103 */\r
104 leadingBufferZone: 200,\r
105\r
106 /**\r
107 * @cfg {Number} defaultViewSize The default view size to use until the {@link #viewSize} has been configured.\r
108 * @private\r
109 */\r
110 defaultViewSize: 100, \r
111\r
112 /**\r
113 * @cfg {Number} viewSize The view size needed to fill the current view. Defaults to the {@link #defaultViewSize}.\r
114 * This will typically be set by the underlying view.\r
115 * @private\r
116 */\r
117 viewSize: 0,\r
118\r
119 /**\r
120 * @inheritdoc\r
121 */\r
122 trackRemoved: false\r
123 },\r
124\r
125 /**\r
126 * We are using applyData so that we can return nothing and prevent the `this.data`\r
127 * property to be overridden.\r
128 * @param {Array/Object} data\r
129 */\r
130 applyData: function(data) {\r
131 var dataCollection = this.data || (this.data = this.createDataCollection());\r
132\r
133 //<debug>\r
134 if (data && data !== true) {\r
135 Ext.raise('Cannot load a buffered store with local data - the store is a map of remote data');\r
136 }\r
137 //</debug>\r
138\r
139 return dataCollection;\r
140 },\r
141\r
142 applyProxy: function(proxy) {\r
143 proxy = this.callParent([proxy]);\r
144\r
145 // This store asks for pages.\r
146 // If used with a MemoryProxy, it must work\r
147 if (proxy && proxy.setEnablePaging) {\r
148 proxy.setEnablePaging(true);\r
149 }\r
150 return proxy;\r
151 },\r
152\r
153 createFiltersCollection: function() {\r
154 return new Ext.util.FilterCollection();\r
155 },\r
156\r
157 createSortersCollection: function() {\r
158 return new Ext.util.SorterCollection();\r
159 },\r
160\r
161 //<debug>\r
162 updateRemoteFilter: function(remoteFilter, oldRemoteFilter) {\r
163 if (remoteFilter === false) {\r
164 Ext.raise('Buffered stores are always remotely filtered.');\r
165 }\r
166 this.callParent([remoteFilter, oldRemoteFilter]);\r
167 },\r
168\r
169 updateRemoteSort: function(remoteSort, oldRemoteSort) {\r
170 if (remoteSort === false) {\r
171 Ext.raise('Buffered stores are always remotely sorted.');\r
172 }\r
173 this.callParent([remoteSort, oldRemoteSort]);\r
174 },\r
175\r
176 updateTrackRemoved: function(value) {\r
177 if (value !== false) {\r
178 Ext.raise('Cannot use trackRemoved with a buffered store.');\r
179 }\r
180 this.callParent(arguments);\r
181 },\r
182 //</debug>\r
183\r
184 updateGroupField: function(field) {\r
185 this.group(field);\r
186 },\r
187\r
188 getGrouper: function() {\r
189 return this.grouper;\r
190 },\r
191\r
192 isGrouped: function() {\r
193 return !!this.grouper;\r
194 },\r
195\r
196 createDataCollection: function() {\r
197 var me = this,\r
198 result = new Ext.data.PageMap({\r
199 store: me,\r
200 rootProperty: 'data',\r
201 pageSize: me.getPageSize(),\r
202 maxSize: me.getPurgePageCount(),\r
203 listeners: {\r
204 // Whenever PageMap gets cleared, it means we re no longer interested in \r
205 // any outstanding page prefetches, so cancel tham all\r
206 clear: me.onPageMapClear,\r
207 scope: me\r
208 }\r
209 });\r
210\r
211 // Allow view to veto prune if the old page is still in use by the view\r
212 me.relayEvents(result, ['beforepageremove', 'pageadd', 'pageremove']);\r
213 me.pageRequests = {};\r
214 return result;\r
215 },\r
216\r
217 //<debug>\r
218 add: function() {\r
219 Ext.raise('add method may not be called on a buffered store - the store is a map of remote data');\r
220 },\r
221 \r
222 insert: function() {\r
223 Ext.raise('insert method may not be called on a buffered store - the store is a map of remote data');\r
224 },\r
225 //</debug>\r
226\r
227 removeAll: function(silent) {\r
228 var me = this,\r
229 data = me.getData();\r
230\r
231 if (data) {\r
232 if (silent) {\r
233 me.suspendEvent('clear');\r
234 }\r
235 data.clear();\r
236 if (silent) {\r
237 me.resumeEvent('clear');\r
238 }\r
239 } \r
240 },\r
241\r
242 flushLoad: function() {\r
243 var me = this,\r
244 options = me.pendingLoadOptions;\r
245\r
246 // If it gets called programatically, the listener will need cancelling\r
247 me.clearLoadTask();\r
248 if (!options) {\r
249 return;\r
250 }\r
251\r
252 // Buffered stores, a load operation means kick off a clean load from page 1\r
253 me.getData().clear();\r
254 options.page = 1;\r
255 options.start = 0;\r
256 options.limit = me.getViewSize() || me.getDefaultViewSize();\r
257\r
258 // If we're prefetching, the arguments on the callback for getting the range is different\r
259 // So we indicate that we need to fire a special "load" style callback\r
260 options.loadCallback = options.callback;\r
261\r
262 // options might be chained, with callback on a prototype; delete won't clear it.\r
263 options.callback = null;\r
264 return me.loadToPrefetch(options);\r
265 },\r
266\r
267 reload: function(options) {\r
268 var me = this,\r
269 data = me.getData(),\r
270 // If we don't have a known totalCount, use a huge value\r
271 lastTotal = Number.MAX_VALUE,\r
272 startIdx, endIdx, startPage, endPage,\r
273 i, waitForReload, bufferZone, records;\r
274\r
275 if (!options) {\r
276 options = {};\r
277 }\r
278\r
279 // Prevent re-entering the load process if we are already in a wait state for a batch of pages.\r
280 if (me.loading || me.fireEvent('beforeload', me, options) === false) {\r
281 return;\r
282 }\r
283\r
284 waitForReload = function() {\r
285 var newCount = me.totalCount,\r
286 oldRequestSize = endIdx - startIdx;\r
287\r
288 // If the dataset has now shrunk leaving the calculated request zone unavailable,\r
289 // re-evaluate the request zone. Start as close to the end as possible.\r
290 if (endIdx >= newCount) {\r
291 endIdx = newCount - 1;\r
292 startIdx = Math.max(endIdx - oldRequestSize, 0);\r
293 }\r
294 if (me.rangeCached(startIdx, Math.min(endIdx, me.totalCount))) {\r
295 me.loading = false;\r
296 data.un('pageadd', waitForReload);\r
297 records = data.getRange(startIdx, endIdx + 1);\r
298 me.fireEvent('load', me, records, true);\r
299 me.fireEvent('refresh', me);\r
300 }\r
301 };\r
302 bufferZone = Math.ceil((me.getLeadingBufferZone() + me.getTrailingBufferZone()) / 2);\r
303\r
304 // Decide what reload means.\r
305 // If the View was configured preserveScrollOnReload, then it will\r
306 // inject that setting here. This means that reload means\r
307 // load the last requested range.\r
308 if (me.lastRequestStart && me.preserveScrollOnReload) {\r
309 startIdx = me.lastRequestStart;\r
310 endIdx = me.lastRequestEnd;\r
311 lastTotal = me.getTotalCount();\r
312 }\r
313 // Otherwise, reload means start from page 1\r
314 else {\r
315 startIdx = options.start || 0;\r
316 endIdx = startIdx + (options.count || me.getPageSize()) - 1;\r
317 }\r
318\r
319 // Clear page cache\r
320 data.clear(true);\r
321\r
322 // So that prefetchPage does not consider the store to be fully loaded if the local count is equal to the total count\r
323 delete me.totalCount;\r
324\r
325 // Calculate a page range which encompasses the Store's loaded range plus both buffer zones\r
326 startIdx = Math.max(startIdx - bufferZone, 0);\r
327 endIdx = Math.min(endIdx + bufferZone, lastTotal);\r
328 startPage = me.getPageFromRecordIndex(startIdx);\r
329 endPage = me.getPageFromRecordIndex(endIdx);\r
330\r
331 me.loading = true;\r
332 options.waitForReload = waitForReload;\r
333\r
334 // Wait for the requested range to become available in the page map\r
335 // Load the range as soon as the whole range is available\r
336 data.on('pageadd', waitForReload);\r
337\r
338 // Recache the page range which encapsulates our visible records\r
339 for (i = startPage; i <= endPage; i++) {\r
340 me.prefetchPage(i, options);\r
341 }\r
342 },\r
343\r
344 filter: function() {\r
345 //<debug>\r
346 if (!this.getRemoteFilter()) {\r
347 Ext.raise('Local filtering may not be used on a buffered store - the store is a map of remote data');\r
348 }\r
349 //</debug>\r
350\r
351 // Remote filtering forces a load. load clears the store's contents.\r
352 this.callParent(arguments);\r
353 },\r
354\r
355 filterBy: function(fn, scope) {\r
356 //<debug>\r
357 Ext.raise('Local filtering may not be used on a buffered store - the store is a map of remote data');\r
358 //</debug>\r
359 },\r
360\r
361 loadData: function(data, append) {\r
362 //<debug>\r
363 Ext.raise('LoadData may not be used on a buffered store - the store is a map of remote data');\r
364 //</debug>\r
365 },\r
366\r
367 loadPage: function(page, options) {\r
368 var me = this;\r
369 options = options || {};\r
370\r
371 options.page = me.currentPage = page;\r
372 options.start = (page - 1) * me.getPageSize();\r
373 options.limit = me.getViewSize() || me.getDefaultViewSize();\r
374 options.loadCallback = options.callback;\r
375\r
376 // options might be chained, with callback on a prototype; delete won't clear it.\r
377 options.callback = null;\r
378 return me.loadToPrefetch(options);\r
379 },\r
380\r
381 clearData: function(isLoad) {\r
382 var me = this,\r
383 data = me.getData();\r
384\r
385 if (data) {\r
386 data.clear();\r
387 }\r
388 },\r
389\r
390 /**\r
391 * @private\r
392 * A BufferedStore always reports that it contains the full dataset.\r
393 * The number of records that happen to be cached at any one time is never useful.\r
394 */\r
395 getCount: function() {\r
396 return this.totalCount || 0;\r
397 },\r
398\r
399 getRange: function(start, end, options) {\r
400 var me = this,\r
401 maxIndex = me.totalCount - 1,\r
402 lastRequestStart = me.lastRequestStart,\r
403 result = [],\r
404 data = me.getData(),\r
405 pageAddHandler,\r
406 requiredStart, requiredEnd,\r
407 requiredStartPage, requiredEndPage;\r
408\r
409 options = Ext.apply({\r
410 prefetchStart: start,\r
411 prefetchEnd: end\r
412 }, options);\r
413\r
414 // Sanity check end point to be within dataset range\r
415 end = (end >= me.totalCount) ? maxIndex : end;\r
416\r
417 // We must wait for a slightly wider range to be cached.\r
418 // This is to allow grouping features to peek at the two surrounding records\r
419 // when rendering a *range* of records to see whether the start of the range\r
420 // really is a group start and the end of the range really is a group end.\r
421 requiredStart = start === 0 ? 0 : start - 1;\r
422 requiredEnd = end === maxIndex ? end : end + 1;\r
423\r
424 // Keep track of range we are being asked for so we can track direction of movement through the dataset\r
425 me.lastRequestStart = start;\r
426 me.lastRequestEnd = end;\r
427\r
428 // If data request can be satisfied from the page cache\r
429 if (me.rangeCached(requiredStart, requiredEnd)) {\r
430 me.onRangeAvailable(options);\r
431 result = data.getRange(start, end + 1);\r
432 }\r
433 // At least some of the requested range needs loading from server\r
434 else {\r
435 // Private event used by the LoadMask class to perform masking when the range required for rendering is not found in the cache\r
436 me.fireEvent('cachemiss', me, start, end);\r
437\r
438 requiredStartPage = me.getPageFromRecordIndex(requiredStart);\r
439 requiredEndPage = me.getPageFromRecordIndex(requiredEnd);\r
440\r
441 // Add a pageadd listener, and as soon as the requested range is loaded, call onRangeAvailable to call the callback.\r
442 pageAddHandler = function(pageMap, page, records) {\r
443 if (page >= requiredStartPage && page <= requiredEndPage && me.rangeCached(requiredStart, requiredEnd)) {\r
444 // Private event used by the LoadMask class to unmask when the range required for rendering has been loaded into the cache\r
445 me.fireEvent('cachefilled', me, start, end);\r
446 data.un('pageadd', pageAddHandler);\r
447 me.onRangeAvailable(options);\r
448 }\r
449 };\r
450 data.on('pageadd', pageAddHandler);\r
451\r
452 // Prioritize the request for the *exact range that the UI is asking for*.\r
453 // When a page request is in flight, it will not be requested again by checking the me.pageRequests hash,\r
454 // so the request after this will only request the *remaining* unrequested pages .\r
455 me.prefetchRange(start, end);\r
456\r
457 }\r
458 // Load the pages around the requested range required by the leadingBufferZone and trailingBufferZone.\r
459 me.primeCache(start, end, start < lastRequestStart ? -1 : 1);\r
460\r
461 return result;\r
462 },\r
463 \r
464 /**\r
465 * Get the Record with the specified id.\r
466 *\r
467 * This method is not affected by filtering, lookup will be performed from all records\r
468 * inside the store, filtered or not.\r
469 *\r
470 * @param {Mixed} id The id of the Record to find.\r
471 * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.\r
472 */\r
473 getById: function(id) {\r
474 var result = this.data.findBy(function(record) {\r
475 return record.getId() === id;\r
476 });\r
477 return result;\r
478 },\r
479\r
480 /**\r
481 * @inheritdoc\r
482 */\r
483 getAt: function(index) {\r
484 var data = this.getData();\r
485\r
486 if (data.hasRange(index, index)) {\r
487 return data.getAt(index);\r
488 }\r
489 },\r
490\r
491 /**\r
492 * @private\r
493 * Get the Record with the specified internalId.\r
494 *\r
495 * This method is not effected by filtering, lookup will be performed from all records\r
496 * inside the store, filtered or not.\r
497 *\r
498 * @param {Mixed} internalId The id of the Record to find.\r
499 * @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.\r
500 */\r
501 getByInternalId: function(internalId) {\r
502 return this.data.getByInternalId(internalId);\r
503 },\r
504\r
505 // Inherit docs\r
506 contains: function(record) {\r
507 return this.indexOf(record) > -1;\r
508 },\r
509\r
510 /**\r
511 * Get the index of the record within the store.\r
512 *\r
513 * When store is filtered, records outside of filter will not be found.\r
514 *\r
515 * @param {Ext.data.Model} record The Ext.data.Model object to find.\r
516 * @return {Number} The index of the passed Record. Returns -1 if not found.\r
517 */\r
518 indexOf: function(record) {\r
519 return this.getData().indexOf(record);\r
520 },\r
521\r
522 /**\r
523 * Get the index within the store of the Record with the passed id.\r
524 *\r
525 * Like #indexOf, this method is effected by filtering.\r
526 *\r
527 * @param {String} id The id of the Record to find.\r
528 * @return {Number} The index of the Record. Returns -1 if not found.\r
529 */\r
530 indexOfId: function(id) {\r
531 return this.indexOf(this.getById(id));\r
532 },\r
533\r
534 group: function(grouper, direction) {\r
535 var me = this,\r
536 oldGrouper;\r
537 \r
538 if (grouper && typeof grouper === 'string') {\r
539 oldGrouper = me.grouper;\r
540\r
541 if (!oldGrouper) {\r
542 me.grouper = new Ext.util.Grouper({\r
543 property : grouper,\r
544 direction: direction || 'ASC',\r
545 root: 'data'\r
546 });\r
547 } else if (direction === undefined) {\r
548 oldGrouper.toggle();\r
549 } else {\r
550 oldGrouper.setDirection(direction);\r
551 }\r
552 } else {\r
553 me.grouper = grouper ? me.getSorters().decodeSorter(grouper, 'Ext.util.Grouper') : null;\r
554 }\r
555\r
556 me.getData().clear();\r
557 me.loadPage(1, {\r
558 callback: function() {\r
559 me.fireEvent('groupchange', me, me.getGrouper());\r
560 }\r
561 });\r
562 },\r
563\r
564 /**\r
565 * Determines the page from a record index\r
566 * @param {Number} index The record index\r
567 * @return {Number} The page the record belongs to\r
568 */\r
569 getPageFromRecordIndex: function(index) {\r
570 return Math.floor(index / this.getPageSize()) + 1;\r
571 },\r
572 \r
573 calculatePageCacheSize: function(rangeSizeRequested) {\r
574 var me = this,\r
575 purgePageCount = me.getPurgePageCount();\r
576\r
577 // Calculate the number of pages that the cache will keep before purging as follows:\r
578 // TWO full rendering zones (in case of rapid teleporting by dragging the scroller) plus configured purgePageCount.\r
579 // Ensure we never reduce the count. It always uses the largest requested block as the basis for the calculated size.\r
580 return purgePageCount ? Math.max(me.getData().getMaxSize() || 0, Math.ceil((rangeSizeRequested + me.getTrailingBufferZone() + me.getLeadingBufferZone()) / me.getPageSize()) * 2 + purgePageCount) : 0;\r
581 },\r
582\r
583 loadToPrefetch: function(options) {\r
584 var me = this,\r
585 prefetchOptions = options,\r
586 i,\r
587 records,\r
588 dataSetSize,\r
589\r
590 // Get the requested record index range in the dataset\r
591 startIdx = options.start,\r
592 endIdx = options.start + options.limit - 1,\r
593 rangeSizeRequested = (me.getViewSize() || options.limit),\r
594\r
595 // The end index to load into the store's live record collection\r
596 loadEndIdx = Math.min(endIdx, options.start + rangeSizeRequested - 1),\r
597\r
598 // Calculate a page range which encompasses the requested range plus both buffer zones.\r
599 // The endPage will be adjusted to be in the dataset size range as soon as the first data block returns.\r
600 startPage = me.getPageFromRecordIndex(Math.max(startIdx - me.getTrailingBufferZone(), 0)),\r
601 endPage = me.getPageFromRecordIndex(endIdx + me.getLeadingBufferZone()),\r
602\r
603 data = me.getData(),\r
604 callbackFn = function () {\r
605 // See comments in load() for why we need this.\r
606 records = records || [];\r
607\r
608 if (options.loadCallback) {\r
609 options.loadCallback.call(options.scope || me, records, operation, true);\r
610 }\r
611\r
612 if (options.callback) {\r
613 options.callback.call(options.scope || me, records, startIdx || 0, endIdx || 0, options);\r
614 }\r
615 },\r
616 fireEventsFn = function () {\r
617 me.fireEvent('datachanged', me);\r
618 me.fireEvent('refresh', me);\r
619 me.fireEvent('load', me, records, true);\r
620 },\r
621 // Wait for the viewable range to be available.\r
622 waitForRequestedRange = function() {\r
623 if (me.rangeCached(startIdx, loadEndIdx)) {\r
624 me.loading = false;\r
625 records = data.getRange(startIdx, loadEndIdx + 1);\r
626 data.un('pageadd', waitForRequestedRange);\r
627\r
628 // If there is a listener for guaranteedrange then fire that event\r
629 if (me.hasListeners.guaranteedrange) {\r
630 me.guaranteeRange(startIdx, loadEndIdx, options.callback, options.scope);\r
631 }\r
632\r
633 callbackFn();\r
634 fireEventsFn();\r
635 }\r
636 }, operation;\r
637\r
638 //<debug>\r
639 if (isNaN(me.pageSize) || !me.pageSize) {\r
640 Ext.raise('Buffered store configured without a pageSize', me);\r
641 }\r
642 //</debug>\r
643\r
644 // Ensure that the purgePageCount allows enough pages to be kept cached to cover the\r
645 // requested range. If the pageSize is very small we might need a lot of pages.\r
646 data.setMaxSize(me.calculatePageCacheSize(rangeSizeRequested));\r
647\r
648 if (me.fireEvent('beforeload', me, options) !== false) {\r
649\r
650 // So that prefetchPage does not consider the store to be fully loaded if the local count is equal to the total count\r
651 delete me.totalCount;\r
652\r
653 me.loading = true;\r
654\r
655 // Any configured callback is handled in waitForRequestedRange above.\r
656 // It should not be processed by onProxyPrefetch.\r
657 if (options.callback) {\r
658 prefetchOptions = Ext.apply({}, options);\r
659 delete prefetchOptions.callback;\r
660 }\r
661\r
662 // Load the first page in the range, which will give us the initial total count.\r
663 // Once it is loaded, go ahead and prefetch any subsequent pages, if necessary.\r
664 // The prefetchPage has a check to prevent us loading more than the totalCount,\r
665 // so we don't want to blindly load up <n> pages where it isn't required.\r
666 me.on('prefetch', function(store, records, successful, op) {\r
667 // Capture operation here so it can be used in the loadCallback above\r
668 operation = op;\r
669 if (successful) {\r
670 // If there is data in the dataset, we can go ahead and add the pageadd listener which waits for the visible range\r
671 // and we can also issue the requests to fill the surrounding buffer zones.\r
672 if ((dataSetSize = me.getTotalCount())) {\r
673\r
674 // Wait for the requested range to become available in the page map\r
675 data.on('pageadd', waitForRequestedRange);\r
676\r
677 // As soon as we have the size of the dataset, ensure we are not waiting for more than can ever arrive,\r
678 loadEndIdx = Math.min(loadEndIdx, dataSetSize - 1);\r
679\r
680 // And make sure we never ask for pages beyond the end of the dataset.\r
681 endPage = me.getPageFromRecordIndex(Math.min(loadEndIdx + me.getLeadingBufferZone(), dataSetSize - 1));\r
682\r
683 for (i = startPage + 1; i <= endPage; ++i) {\r
684 me.prefetchPage(i, prefetchOptions);\r
685 }\r
686 } else {\r
687 callbackFn();\r
688 fireEventsFn();\r
689 }\r
690 }\r
691 // Unsuccessful prefetch: fire a load event with success false.\r
692 else {\r
693 me.loading = false;\r
694 callbackFn();\r
695 me.fireEvent('load', me, records, false);\r
696 }\r
697 }, null, {single: true});\r
698\r
699 me.prefetchPage(startPage, prefetchOptions);\r
700 }\r
701 },\r
702\r
703 // Buffering\r
704 /**\r
705 * Prefetches data into the store using its configured {@link #proxy}.\r
706 * @param {Object} options (Optional) config object, passed into the Ext.data.operation.Operation object before loading.\r
707 * See {@link #method-load}\r
708 */\r
709 prefetch: function(options) {\r
710 var me = this,\r
711 pageSize = me.getPageSize(),\r
712 data = me.getData(),\r
713 operation,\r
714 existingPageRequest;\r
715\r
716 // Check pageSize has not been tampered with. That would break page caching\r
717 if (pageSize) {\r
718 if (me.lastPageSize && pageSize != me.lastPageSize) {\r
719 Ext.raise("pageSize cannot be dynamically altered");\r
720 }\r
721 if (!data.getPageSize()) {\r
722 data.setPageSize(pageSize);\r
723 }\r
724 }\r
725\r
726 // Allow first prefetch call to imply the required page size.\r
727 else {\r
728 me.pageSize = data.setPageSize(pageSize = options.limit);\r
729 }\r
730\r
731 // So that we can check for tampering next time through\r
732 me.lastPageSize = pageSize;\r
733\r
734 // Always get whole pages.\r
735 if (!options.page) {\r
736 options.page = me.getPageFromRecordIndex(options.start);\r
737 options.start = (options.page - 1) * pageSize;\r
738 options.limit = Math.ceil(options.limit / pageSize) * pageSize;\r
739 }\r
740\r
741 // Currently not requesting this page, or the request was for the last\r
742 // generation of the data cache (clearing it changes generations)\r
743 // then request it...\r
744 existingPageRequest = me.pageRequests[options.page];\r
745 if (!existingPageRequest || existingPageRequest.getOperation().pageMapGeneration !== data.pageMapGeneration) {\r
746 // Copy options into a new object so as not to mutate passed in objects\r
747 options = Ext.apply({\r
748 action : 'read',\r
749 filters: me.getFilters().items,\r
750 sorters: me.getSorters().items,\r
751 grouper: me.getGrouper(),\r
752 internalCallback: me.onProxyPrefetch,\r
753 internalScope: me\r
754 }, options);\r
755\r
756 operation = me.createOperation('read', options);\r
757\r
758 // Generation # of the page map to which the requested records belong.\r
759 // If page map is cleared while this request is in flight, the pageMapGeneration will increment and the payload will be rejected\r
760 operation.pageMapGeneration = data.pageMapGeneration;\r
761\r
762 if (me.fireEvent('beforeprefetch', me, operation) !== false) {\r
763 me.pageRequests[options.page] = operation.execute();\r
764 if (me.getProxy().isSynchronous) {\r
765 delete me.pageRequests[options.page];\r
766 }\r
767 }\r
768 }\r
769\r
770 return me;\r
771 },\r
772\r
773 /**\r
774 * @private\r
775 * Cancels all pending prefetch requests.\r
776 *\r
777 * This is called when the page map is cleared.\r
778 *\r
779 * Any requests which still make it through will be for the previous pageMapGeneration\r
780 * (pageMapGeneration is incremented upon clear), and so will be rejected upon arrival.\r
781 */\r
782 onPageMapClear: function() {\r
783 var me = this,\r
784 loadingFlag = me.wasLoading,\r
785 reqs = me.pageRequests,\r
786 data = me.getData(),\r
787 page;\r
788\r
789 // If any requests return, we no longer respond to them.\r
790 data.clearListeners();\r
791\r
792 // replace the listeners we need.\r
793 data.on('clear', me.onPageMapClear, me);\r
794 me.relayEvents(data, ['beforepageremove', 'pageadd', 'pageremove']);\r
795\r
796 // If the page cache gets cleared it's because a full reload is in progress.\r
797 // Setting the loading flag prevents linked Views from displaying the empty text\r
798 // during a load... we don't know whether ther dataset is empty or not.\r
799 me.loading = true;\r
800 me.totalCount = 0;\r
801\r
802 // Abort all outstanding requests.\r
803 // onProxyPrefetch will reject them as being for the previous data generation\r
804 // anyway, if they do return.\r
805 // because of the pageMapGeneration mismatch.\r
806 for (page in reqs) {\r
807 if (reqs.hasOwnProperty(page)) {\r
808 reqs[page].getOperation().abort();\r
809 }\r
810 }\r
811\r
812 // This will update any views. \r
813 me.fireEvent('clear', me);\r
814\r
815 // Restore loading flag. The beforeload event could still veto the process.\r
816 // The flag does not get set for real until we pass the beforeload event.\r
817 me.loading = loadingFlag;\r
818 },\r
819\r
820 /**\r
821 * Prefetches a page of data.\r
822 * @param {Number} page The page to prefetch\r
823 * @param {Object} options (Optional) config object, passed into the Ext.data.operation.Operation object before loading.\r
824 * See {@link #method-load}\r
825 */\r
826 prefetchPage: function(page, options) {\r
827 var me = this,\r
828 pageSize = me.getPageSize(),\r
829 start = (page - 1) * pageSize,\r
830 total = me.totalCount;\r
831\r
832 // No more data to prefetch.\r
833 if (total !== undefined && me.data.getCount() === total) {\r
834 return;\r
835 }\r
836\r
837 // Copy options into a new object so as not to mutate passed in objects\r
838 me.prefetch(Ext.applyIf({\r
839 page : page,\r
840 start : start,\r
841 limit : pageSize\r
842 }, options));\r
843 },\r
844\r
845 /**\r
846 * Called after the configured proxy completes a prefetch operation.\r
847 * @private\r
848 * @param {Ext.data.operation.Operation} operation The operation that completed\r
849 */\r
850 onProxyPrefetch: function(operation) {\r
851 if (this.destroyed) {\r
852 return;\r
853 }\r
854 \r
855 var me = this,\r
856 resultSet = operation.getResultSet(),\r
857 records = operation.getRecords(),\r
858 successful = operation.wasSuccessful(),\r
859 page = operation.getPage(),\r
860 waitForReload = operation.waitForReload,\r
861 oldTotal = me.totalCount,\r
862 requests = me.pageRequests,\r
863 key, op;\r
864\r
865 // Only cache the data if the operation was invoked for the current pageMapGeneration.\r
866 // If the pageMapGeneration has changed since the request was fired off, it will have been cancelled.\r
867 if (operation.pageMapGeneration === me.getData().pageMapGeneration) {\r
868\r
869 if (resultSet) {\r
870 me.totalCount = resultSet.getTotal();\r
871 if (me.totalCount !== oldTotal) {\r
872 me.fireEvent('totalcountchange', me.totalCount);\r
873 }\r
874 }\r
875\r
876 // Remove the loaded page from the outstanding pages hash\r
877 if (page !== undefined) {\r
878 delete me.pageRequests[page];\r
879 }\r
880\r
881 // Prefetch is broadcast before the page is cached\r
882 me.loading = false;\r
883 me.fireEvent('prefetch', me, records, successful, operation);\r
884\r
885 // Add the page into the page map.\r
886 // pageadd event may trigger the onRangeAvailable\r
887 if (successful) {\r
888 if (me.totalCount === 0) {\r
889 if (waitForReload) {\r
890 for (key in requests) {\r
891 op = requests[key].getOperation();\r
892 // Created in the same batch, clear the waitForReload so this\r
893 // won't be run again\r
894 if (op.waitForReload === waitForReload) {\r
895 delete op.waitForReload;\r
896 }\r
897 }\r
898 me.getData().un('pageadd', waitForReload);\r
899 me.fireEvent('load', me, [], true);\r
900 me.fireEvent('refresh', me);\r
901 }\r
902 } else {\r
903 me.cachePage(records, operation.getPage());\r
904 }\r
905 }\r
906\r
907 //this is a callback that would have been passed to the 'read' function and is optional\r
908 Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);\r
909 }\r
910 },\r
911\r
912 /**\r
913 * Caches the records in the prefetch and stripes them with their server-side\r
914 * index.\r
915 * @private\r
916 * @param {Ext.data.Model[]} records The records to cache\r
917 * @param {Ext.data.operation.Operation} page The associated operation\r
918 */\r
919 cachePage: function(records, page) {\r
920 var me = this,\r
921 len = records.length, i;\r
922\r
923 if (!Ext.isDefined(me.totalCount)) {\r
924 me.totalCount = records.length;\r
925 me.fireEvent('totalcountchange', me.totalCount);\r
926 }\r
927\r
928 // Add the fetched page into the pageCache\r
929 for (i = 0; i < len; i++) {\r
930 records[i].join(me);\r
931 }\r
932 me.getData().addPage(page, records);\r
933 },\r
934\r
935 /**\r
936 * Determines if the passed range is available in the page cache.\r
937 * @private\r
938 * @param {Number} start The start index\r
939 * @param {Number} end The end index in the range\r
940 */\r
941 rangeCached: function(start, end) {\r
942 return this.getData().hasRange(start, end);\r
943 },\r
944\r
945 /**\r
946 * Determines if the passed page is available in the page cache.\r
947 * @private\r
948 * @param {Number} page The page to find in the page cache.\r
949 */\r
950 pageCached: function(page) {\r
951 return this.getData().hasPage(page);\r
952 },\r
953\r
954 /**\r
955 * Determines if a request for a page is currently running\r
956 * @private\r
957 * @param {Number} page The page to check for\r
958 */\r
959 pagePending: function(page) {\r
960 return !!this.pageRequests[page];\r
961 },\r
962\r
963 /**\r
964 * Determines if the passed range is available in the page cache.\r
965 * @private\r
966 * @deprecated 4.1.0 use {@link #rangeCached} instead\r
967 * @param {Number} start The start index\r
968 * @param {Number} end The end index in the range\r
969 * @return {Boolean}\r
970 */\r
971 rangeSatisfied: function(start, end) {\r
972 return this.rangeCached(start, end);\r
973 },\r
974\r
975 /**\r
976 * Handles the availability of a requested range that was not previously available\r
977 * @private\r
978 */\r
979 onRangeAvailable: function(options) {\r
980 var me = this,\r
981 totalCount = me.getTotalCount(),\r
982 start = options.prefetchStart,\r
983 end = (options.prefetchEnd > totalCount - 1) ? totalCount - 1 : options.prefetchEnd,\r
984 range;\r
985\r
986 end = Math.max(0, end);\r
987\r
988 //<debug>\r
989 if (start > end) {\r
990 Ext.log({\r
991 level: 'warn',\r
992 msg: 'Start (' + start + ') was greater than end (' + end +\r
993 ') for the range of records requested (' + start + '-' +\r
994 options.prefetchEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')\r
995 });\r
996 }\r
997 //</debug>\r
998\r
999 range = me.getData().getRange(start, end + 1);\r
1000 if (options.fireEvent !== false) {\r
1001 me.fireEvent('guaranteedrange', range, start, end, options);\r
1002 }\r
1003 if (options.callback) {\r
1004 options.callback.call(options.scope || me, range, start, end, options);\r
1005 }\r
1006 },\r
1007\r
1008 /**\r
1009 * Guarantee a specific range, this will load the store with a range (that\r
1010 * must be the `pageSize` or smaller) and take care of any loading that may\r
1011 * be necessary.\r
1012 * @deprecated Use {@link #getRange}\r
1013 */\r
1014 guaranteeRange: function(start, end, callback, scope, options) {\r
1015 options = Ext.apply({\r
1016 callback: callback,\r
1017 scope: scope\r
1018 }, options);\r
1019 this.getRange(start, end + 1, options);\r
1020 },\r
1021\r
1022 /**\r
1023 * Ensures that the specified range of rows is present in the cache.\r
1024 *\r
1025 * Converts the row range to a page range and then only load pages which are not already\r
1026 * present in the page cache.\r
1027 */\r
1028 prefetchRange: function(start, end) {\r
1029 var me = this,\r
1030 startPage, endPage, page,\r
1031 data = me.getData();\r
1032\r
1033 if (!me.rangeCached(start, end)) {\r
1034 startPage = me.getPageFromRecordIndex(start);\r
1035 endPage = me.getPageFromRecordIndex(end);\r
1036\r
1037 // Ensure that the page cache's max size is correct.\r
1038 // Our purgePageCount is the number of additional pages *outside of the required range* which\r
1039 // may be kept in the cache. A purgePageCount of zero means unlimited.\r
1040 data.setMaxSize(me.calculatePageCacheSize(end - start + 1));\r
1041\r
1042 // We have the range, but ensure that we have a "buffer" of pages around it.\r
1043 for (page = startPage; page <= endPage; page++) {\r
1044 if (!me.pageCached(page)) {\r
1045 me.prefetchPage(page);\r
1046 }\r
1047 }\r
1048 }\r
1049 },\r
1050\r
1051 primeCache: function(start, end, direction) {\r
1052 var me = this,\r
1053 leadingBufferZone = me.getLeadingBufferZone(),\r
1054 trailingBufferZone = me.getTrailingBufferZone(),\r
1055 pageSize = me.getPageSize(),\r
1056 totalCount = me.totalCount;\r
1057\r
1058 // Scrolling up\r
1059 if (direction === -1) {\r
1060 start = Math.max(start - leadingBufferZone, 0);\r
1061 end = Math.min(end + trailingBufferZone, totalCount - 1);\r
1062 }\r
1063 // Scrolling down\r
1064 else if (direction === 1) {\r
1065 start = Math.max(Math.min(start - trailingBufferZone, totalCount - pageSize), 0);\r
1066 end = Math.min(end + leadingBufferZone, totalCount - 1);\r
1067 }\r
1068 // Teleporting\r
1069 else {\r
1070 start = Math.min(Math.max(Math.floor(start - ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - me.pageSize);\r
1071 end = Math.min(Math.max(Math.ceil (end + ((leadingBufferZone + trailingBufferZone) / 2)), 0), totalCount - 1);\r
1072 }\r
1073 me.prefetchRange(start, end);\r
1074 },\r
1075\r
1076 sort: function(field, direction, mode) {\r
1077 if (arguments.length === 0) {\r
1078 this.clearAndLoad();\r
1079 } else {\r
1080 this.getSorters().addSort(field, direction, mode);\r
1081 }\r
1082 },\r
1083\r
1084 onSorterEndUpdate: function() {\r
1085 var me = this,\r
1086 sorters = me.getSorters().getRange();\r
1087\r
1088 // Only load or sort if there are sorters\r
1089 if (sorters.length) {\r
1090 me.fireEvent('beforesort', me, sorters);\r
1091 me.clearAndLoad({\r
1092 callback: function() {\r
1093 me.fireEvent('sort', me, sorters);\r
1094 }\r
1095 });\r
1096 } else {\r
1097 // Sort event must fire when sorters collection is updated to empty.\r
1098 me.fireEvent('sort', me, sorters);\r
1099 }\r
1100 },\r
1101\r
1102 clearAndLoad: function (options) {\r
1103 this.getData().clear();\r
1104 this.loadPage(1, options);\r
1105 },\r
1106\r
1107 privates: {\r
1108 isLast: function(record) {\r
1109 return this.indexOf(record) === this.getTotalCount() - 1;\r
1110 },\r
1111\r
1112 isMoving: function () {\r
1113 return false;\r
1114 }\r
1115 }\r
1116});\r