Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / Store.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @author Ed Spencer
17  * @class Ext.data.Store
18  * @extends Ext.data.AbstractStore
19  *
20  * <p>The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
21  * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
22  * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.</p>
23  *
24  * <p>Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:</p>
25  *
26 <pre><code>
27 // Set up a {@link Ext.data.Model model} to use in our Store
28 Ext.define('User', {
29     extend: 'Ext.data.Model',
30     fields: [
31         {name: 'firstName', type: 'string'},
32         {name: 'lastName',  type: 'string'},
33         {name: 'age',       type: 'int'},
34         {name: 'eyeColor',  type: 'string'}
35     ]
36 });
37
38 var myStore = Ext.create('Ext.data.Store', {
39     model: 'User',
40     proxy: {
41         type: 'ajax',
42         url : '/users.json',
43         reader: {
44             type: 'json',
45             root: 'users'
46         }
47     },
48     autoLoad: true
49 });
50 </code></pre>
51
52  * <p>In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
53  * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
54  * {@link Ext.data.reader.Json see the docs on JsonReader} for details.</p>
55  *
56  * <p><u>Inline data</u></p>
57  *
58  * <p>Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data}
59  * into Model instances:</p>
60  *
61 <pre><code>
62 Ext.create('Ext.data.Store', {
63     model: 'User',
64     data : [
65         {firstName: 'Ed',    lastName: 'Spencer'},
66         {firstName: 'Tommy', lastName: 'Maintz'},
67         {firstName: 'Aaron', lastName: 'Conran'},
68         {firstName: 'Jamie', lastName: 'Avins'}
69     ]
70 });
71 </code></pre>
72  *
73  * <p>Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
74  * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
75  * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).</p>
76  *
77  * <p>Additional data can also be loaded locally using {@link #add}.</p>
78  *
79  * <p><u>Loading Nested Data</u></p>
80  *
81  * <p>Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
82  * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset
83  * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
84  * docs for a full explanation:</p>
85  *
86 <pre><code>
87 var store = Ext.create('Ext.data.Store', {
88     autoLoad: true,
89     model: "User",
90     proxy: {
91         type: 'ajax',
92         url : 'users.json',
93         reader: {
94             type: 'json',
95             root: 'users'
96         }
97     }
98 });
99 </code></pre>
100  *
101  * <p>Which would consume a response like this:</p>
102  *
103 <pre><code>
104 {
105     "users": [
106         {
107             "id": 1,
108             "name": "Ed",
109             "orders": [
110                 {
111                     "id": 10,
112                     "total": 10.76,
113                     "status": "invoiced"
114                 },
115                 {
116                     "id": 11,
117                     "total": 13.45,
118                     "status": "shipped"
119                 }
120             ]
121         }
122     ]
123 }
124 </code></pre>
125  *
126  * <p>See the {@link Ext.data.reader.Reader} intro docs for a full explanation.</p>
127  *
128  * <p><u>Filtering and Sorting</u></p>
129  *
130  * <p>Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
131  * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
132  * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
133  *
134 <pre><code>
135 var store = Ext.create('Ext.data.Store', {
136     model: 'User',
137     sorters: [
138         {
139             property : 'age',
140             direction: 'DESC'
141         },
142         {
143             property : 'firstName',
144             direction: 'ASC'
145         }
146     ],
147
148     filters: [
149         {
150             property: 'firstName',
151             value   : /Ed/
152         }
153     ]
154 });
155 </code></pre>
156  *
157  * <p>The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
158  * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
159  * perform these operations instead.</p>
160  *
161  * <p>Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
162  * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
163  * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.</p>
164  *
165 <pre><code>
166 store.filter('eyeColor', 'Brown');
167 </code></pre>
168  *
169  * <p>Change the sorting at any time by calling {@link #sort}:</p>
170  *
171 <pre><code>
172 store.sort('height', 'ASC');
173 </code></pre>
174  *
175  * <p>Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
176  * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
177  * to the MixedCollection:</p>
178  *
179 <pre><code>
180 store.sorters.add(new Ext.util.Sorter({
181     property : 'shoeSize',
182     direction: 'ASC'
183 }));
184
185 store.sort();
186 </code></pre>
187  *
188  * <p><u>Registering with StoreManager</u></p>
189  *
190  * <p>Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}.
191  * This makes it easy to reuse the same store in multiple views:</p>
192  *
193  <pre><code>
194 //this store can be used several times
195 Ext.create('Ext.data.Store', {
196     model: 'User',
197     storeId: 'usersStore'
198 });
199
200 new Ext.List({
201     store: 'usersStore',
202
203     //other config goes here
204 });
205
206 new Ext.view.View({
207     store: 'usersStore',
208
209     //other config goes here
210 });
211 </code></pre>
212  *
213  * <p><u>Further Reading</u></p>
214  *
215  * <p>Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
216  * pieces and how they fit together, see:</p>
217  *
218  * <ul style="list-style-type: disc; padding-left: 25px">
219  * <li>{@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used</li>
220  * <li>{@link Ext.data.Model Model} - the core class in the data package</li>
221  * <li>{@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response</li>
222  * </ul>
223  *
224  */
225 Ext.define('Ext.data.Store', {
226     extend: 'Ext.data.AbstractStore',
227
228     alias: 'store.store',
229
230     requires: ['Ext.data.StoreManager', 'Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'],
231     uses: ['Ext.data.proxy.Memory'],
232
233     /**
234      * @cfg {Boolean} remoteSort
235      * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to <tt>false</tt>.
236      */
237     remoteSort: false,
238
239     /**
240      * @cfg {Boolean} remoteFilter
241      * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to <tt>false</tt>.
242      */
243     remoteFilter: false,
244
245     /**
246      * @cfg {Boolean} remoteGroup
247      * True if the grouping should apply on the server side, false if it is local only.  If the
248      * grouping is local, it can be applied immediately to the data.  If it is remote, then it will simply act as a
249      * helper, automatically sending the grouping information to the server.
250      */
251     remoteGroup : false,
252
253     /**
254      * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
255      * object or a Proxy instance - see {@link #setProxy} for details.
256      */
257
258     /**
259      * @cfg {Object[]/Ext.data.Model[]} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
260      */
261
262     /**
263      * @property {String} groupField
264      * The field by which to group data in the store. Internally, grouping is very similar to sorting - the
265      * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
266      * level of grouping, and groups can be fetched via the {@link #getGroups} method.
267      */
268     groupField: undefined,
269
270     /**
271      * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
272      * @property groupDir
273      * @type String
274      */
275     groupDir: "ASC",
276
277     /**
278      * @cfg {Number} pageSize
279      * The number of records considered to form a 'page'. This is used to power the built-in
280      * paging using the nextPage and previousPage functions. Defaults to 25.
281      */
282     pageSize: 25,
283
284     /**
285      * The page that the Store has most recently loaded (see {@link #loadPage})
286      * @property currentPage
287      * @type Number
288      */
289     currentPage: 1,
290
291     /**
292      * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
293      * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records, allowing
294      * large data sets to be loaded one page at a time but rendered all together.
295      */
296     clearOnPageLoad: true,
297
298     /**
299      * @property {Boolean} loading
300      * True if the Store is currently loading via its Proxy
301      * @private
302      */
303     loading: false,
304
305     /**
306      * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called,
307      * causing the sorters to be reapplied after filtering. Defaults to true
308      */
309     sortOnFilter: true,
310
311     /**
312      * @cfg {Boolean} buffered
313      * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will
314      * tell the store to pre-fetch records ahead of a time.
315      */
316     buffered: false,
317
318     /**
319      * @cfg {Number} purgePageCount
320      * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data.
321      * This option is only relevant when the {@link #buffered} option is set to true.
322      */
323     purgePageCount: 5,
324
325     isStore: true,
326
327     onClassExtended: function(cls, data) {
328         var model = data.model;
329
330         if (typeof model == 'string') {
331             var onBeforeClassCreated = data.onBeforeClassCreated;
332
333             data.onBeforeClassCreated = function(cls, data) {
334                 var me = this;
335
336                 Ext.require(model, function() {
337                     onBeforeClassCreated.call(me, cls, data);
338                 });
339             };
340         }
341     },
342
343     /**
344      * Creates the store.
345      * @param {Object} config (optional) Config object
346      */
347     constructor: function(config) {
348         // Clone the config so we don't modify the original config object
349         config = Ext.Object.merge({}, config);
350
351         var me = this,
352             groupers = config.groupers || me.groupers,
353             groupField = config.groupField || me.groupField,
354             proxy,
355             data;
356
357         if (config.buffered || me.buffered) {
358             me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
359                 return record.index;
360             });
361             me.pendingRequests = [];
362             me.pagesRequested = [];
363
364             me.sortOnLoad = false;
365             me.filterOnLoad = false;
366         }
367
368         me.addEvents(
369             /**
370              * @event beforeprefetch
371              * Fires before a prefetch occurs. Return false to cancel.
372              * @param {Ext.data.Store} this
373              * @param {Ext.data.Operation} operation The associated operation
374              */
375             'beforeprefetch',
376             /**
377              * @event groupchange
378              * Fired whenever the grouping in the grid changes
379              * @param {Ext.data.Store} store The store
380              * @param {Ext.util.Grouper[]} groupers The array of grouper objects
381              */
382             'groupchange',
383             /**
384              * @event load
385              * Fires whenever records have been prefetched
386              * @param {Ext.data.Store} this
387              * @param {Ext.util.Grouper[]} records An array of records
388              * @param {Boolean} successful True if the operation was successful.
389              * @param {Ext.data.Operation} operation The associated operation
390              */
391             'prefetch'
392         );
393         data = config.data || me.data;
394
395         /**
396          * The MixedCollection that holds this store's local cache of records
397          * @property data
398          * @type Ext.util.MixedCollection
399          */
400         me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
401             return record.internalId;
402         });
403
404         if (data) {
405             me.inlineData = data;
406             delete config.data;
407         }
408
409         if (!groupers && groupField) {
410             groupers = [{
411                 property : groupField,
412                 direction: config.groupDir || me.groupDir
413             }];
414         }
415         delete config.groupers;
416
417         /**
418          * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
419          * @property groupers
420          * @type Ext.util.MixedCollection
421          */
422         me.groupers = Ext.create('Ext.util.MixedCollection');
423         me.groupers.addAll(me.decodeGroupers(groupers));
424
425         this.callParent([config]);
426         // don't use *config* anymore from here on... use *me* instead...
427
428         if (me.groupers.items.length) {
429             me.sort(me.groupers.items, 'prepend', false);
430         }
431
432         proxy = me.proxy;
433         data = me.inlineData;
434
435         if (data) {
436             if (proxy instanceof Ext.data.proxy.Memory) {
437                 proxy.data = data;
438                 me.read();
439             } else {
440                 me.add.apply(me, data);
441             }
442
443             me.sort();
444             delete me.inlineData;
445         } else if (me.autoLoad) {
446             Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]);
447             // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here.
448             // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined);
449         }
450     },
451
452     onBeforeSort: function() {
453         var groupers = this.groupers;
454         if (groupers.getCount() > 0) {
455             this.sort(groupers.items, 'prepend', false);
456         }
457     },
458
459     /**
460      * @private
461      * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
462      * @param {Object[]} groupers The groupers array
463      * @return {Ext.util.Grouper[]} Array of Ext.util.Grouper objects
464      */
465     decodeGroupers: function(groupers) {
466         if (!Ext.isArray(groupers)) {
467             if (groupers === undefined) {
468                 groupers = [];
469             } else {
470                 groupers = [groupers];
471             }
472         }
473
474         var length  = groupers.length,
475             Grouper = Ext.util.Grouper,
476             config, i;
477
478         for (i = 0; i < length; i++) {
479             config = groupers[i];
480
481             if (!(config instanceof Grouper)) {
482                 if (Ext.isString(config)) {
483                     config = {
484                         property: config
485                     };
486                 }
487
488                 Ext.applyIf(config, {
489                     root     : 'data',
490                     direction: "ASC"
491                 });
492
493                 //support for 3.x style sorters where a function can be defined as 'fn'
494                 if (config.fn) {
495                     config.sorterFn = config.fn;
496                 }
497
498                 //support a function to be passed as a sorter definition
499                 if (typeof config == 'function') {
500                     config = {
501                         sorterFn: config
502                     };
503                 }
504
505                 groupers[i] = new Grouper(config);
506             }
507         }
508
509         return groupers;
510     },
511
512     /**
513      * Group data in the store
514      * @param {String/Object[]} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model},
515      * or an Array of grouper configurations.
516      * @param {String} direction The overall direction to group the data by. Defaults to "ASC".
517      */
518     group: function(groupers, direction) {
519         var me = this,
520             hasNew = false,
521             grouper,
522             newGroupers;
523
524         if (Ext.isArray(groupers)) {
525             newGroupers = groupers;
526         } else if (Ext.isObject(groupers)) {
527             newGroupers = [groupers];
528         } else if (Ext.isString(groupers)) {
529             grouper = me.groupers.get(groupers);
530
531             if (!grouper) {
532                 grouper = {
533                     property : groupers,
534                     direction: direction
535                 };
536                 newGroupers = [grouper];
537             } else if (direction === undefined) {
538                 grouper.toggle();
539             } else {
540                 grouper.setDirection(direction);
541             }
542         }
543
544         if (newGroupers && newGroupers.length) {
545             hasNew = true;
546             newGroupers = me.decodeGroupers(newGroupers);
547             me.groupers.clear();
548             me.groupers.addAll(newGroupers);
549         }
550
551         if (me.remoteGroup) {
552             me.load({
553                 scope: me,
554                 callback: me.fireGroupChange
555             });
556         } else {
557             // need to explicitly force a sort if we have groupers
558             me.sort(null, null, null, hasNew);
559             me.fireGroupChange();
560         }
561     },
562
563     /**
564      * Clear any groupers in the store
565      */
566     clearGrouping: function(){
567         var me = this;
568         // Clear any groupers we pushed on to the sorters
569         me.groupers.each(function(grouper){
570             me.sorters.remove(grouper);
571         });
572         me.groupers.clear();
573         if (me.remoteGroup) {
574             me.load({
575                 scope: me,
576                 callback: me.fireGroupChange
577             });
578         } else {
579             me.sort();
580             me.fireEvent('groupchange', me, me.groupers);
581         }
582     },
583
584     /**
585      * Checks if the store is currently grouped
586      * @return {Boolean} True if the store is grouped.
587      */
588     isGrouped: function() {
589         return this.groupers.getCount() > 0;
590     },
591
592     /**
593      * Fires the groupchange event. Abstracted out so we can use it
594      * as a callback
595      * @private
596      */
597     fireGroupChange: function(){
598         this.fireEvent('groupchange', this, this.groupers);
599     },
600
601     /**
602      * Returns an array containing the result of applying grouping to the records in this store. See {@link #groupField},
603      * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field:
604 <pre><code>
605 var myStore = Ext.create('Ext.data.Store', {
606     groupField: 'color',
607     groupDir  : 'DESC'
608 });
609
610 myStore.getGroups(); //returns:
611 [
612     {
613         name: 'yellow',
614         children: [
615             //all records where the color field is 'yellow'
616         ]
617     },
618     {
619         name: 'red',
620         children: [
621             //all records where the color field is 'red'
622         ]
623     }
624 ]
625 </code></pre>
626      * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString}
627      * @return {Object/Object[]} The grouped data
628      */
629     getGroups: function(requestGroupString) {
630         var records = this.data.items,
631             length = records.length,
632             groups = [],
633             pointers = {},
634             record,
635             groupStr,
636             group,
637             i;
638
639         for (i = 0; i < length; i++) {
640             record = records[i];
641             groupStr = this.getGroupString(record);
642             group = pointers[groupStr];
643
644             if (group === undefined) {
645                 group = {
646                     name: groupStr,
647                     children: []
648                 };
649
650                 groups.push(group);
651                 pointers[groupStr] = group;
652             }
653
654             group.children.push(record);
655         }
656
657         return requestGroupString ? pointers[requestGroupString] : groups;
658     },
659
660     /**
661      * @private
662      * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records
663      * matching a certain group.
664      */
665     getGroupsForGrouper: function(records, grouper) {
666         var length = records.length,
667             groups = [],
668             oldValue,
669             newValue,
670             record,
671             group,
672             i;
673
674         for (i = 0; i < length; i++) {
675             record = records[i];
676             newValue = grouper.getGroupString(record);
677
678             if (newValue !== oldValue) {
679                 group = {
680                     name: newValue,
681                     grouper: grouper,
682                     records: []
683                 };
684                 groups.push(group);
685             }
686
687             group.records.push(record);
688
689             oldValue = newValue;
690         }
691
692         return groups;
693     },
694
695     /**
696      * @private
697      * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for
698      * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by
699      * {@link #getGroupsForGrouper} - this function largely just handles the recursion.
700      * @param {Ext.data.Model[]} records The set or subset of records to group
701      * @param {Number} grouperIndex The grouper index to retrieve
702      * @return {Object[]} The grouped records
703      */
704     getGroupsForGrouperIndex: function(records, grouperIndex) {
705         var me = this,
706             groupers = me.groupers,
707             grouper = groupers.getAt(grouperIndex),
708             groups = me.getGroupsForGrouper(records, grouper),
709             length = groups.length,
710             i;
711
712         if (grouperIndex + 1 < groupers.length) {
713             for (i = 0; i < length; i++) {
714                 groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
715             }
716         }
717
718         for (i = 0; i < length; i++) {
719             groups[i].depth = grouperIndex;
720         }
721
722         return groups;
723     },
724
725     /**
726      * @private
727      * <p>Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in
728      * this case grouping by genre and then author in a fictional books dataset):</p>
729 <pre><code>
730 [
731     {
732         name: 'Fantasy',
733         depth: 0,
734         records: [
735             //book1, book2, book3, book4
736         ],
737         children: [
738             {
739                 name: 'Rowling',
740                 depth: 1,
741                 records: [
742                     //book1, book2
743                 ]
744             },
745             {
746                 name: 'Tolkein',
747                 depth: 1,
748                 records: [
749                     //book3, book4
750                 ]
751             }
752         ]
753     }
754 ]
755 </code></pre>
756      * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping
757      * function correctly so this should only be set to false if the Store is known to already be sorted correctly
758      * (defaults to true)
759      * @return {Object[]} The group data
760      */
761     getGroupData: function(sort) {
762         var me = this;
763         if (sort !== false) {
764             me.sort();
765         }
766
767         return me.getGroupsForGrouperIndex(me.data.items, 0);
768     },
769
770     /**
771      * <p>Returns the string to group on for a given model instance. The default implementation of this method returns
772      * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to
773      * group by the first letter of a model's 'name' field, use the following code:</p>
774 <pre><code>
775 Ext.create('Ext.data.Store', {
776     groupDir: 'ASC',
777     getGroupString: function(instance) {
778         return instance.get('name')[0];
779     }
780 });
781 </code></pre>
782      * @param {Ext.data.Model} instance The model instance
783      * @return {String} The string to compare when forming groups
784      */
785     getGroupString: function(instance) {
786         var group = this.groupers.first();
787         if (group) {
788             return instance.get(group.property);
789         }
790         return '';
791     },
792     /**
793      * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
794      * See also <code>{@link #add}</code>.
795      * @param {Number} index The start index at which to insert the passed Records.
796      * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
797      */
798     insert: function(index, records) {
799         var me = this,
800             sync = false,
801             i,
802             record,
803             len;
804
805         records = [].concat(records);
806         for (i = 0, len = records.length; i < len; i++) {
807             record = me.createModel(records[i]);
808             record.set(me.modelDefaults);
809             // reassign the model in the array in case it wasn't created yet
810             records[i] = record;
811
812             me.data.insert(index + i, record);
813             record.join(me);
814
815             sync = sync || record.phantom === true;
816         }
817
818         if (me.snapshot) {
819             me.snapshot.addAll(records);
820         }
821
822         me.fireEvent('add', me, records, index);
823         me.fireEvent('datachanged', me);
824         if (me.autoSync && sync) {
825             me.sync();
826         }
827     },
828
829     /**
830      * Adds Model instance to the Store. This method accepts either:
831      *
832      * - An array of Model instances or Model configuration objects.
833      * - Any number of Model instance or Model configuration object arguments.
834      *
835      * The new Model instances will be added at the end of the existing collection.
836      *
837      * Sample usage:
838      *
839      *     myStore.add({some: 'data'}, {some: 'other data'});
840      *
841      * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
842      * or Model configuration objects, or variable number of Model instance or config arguments.
843      * @return {Ext.data.Model[]} The model instances that were added
844      */
845     add: function(records) {
846         //accept both a single-argument array of records, or any number of record arguments
847         if (!Ext.isArray(records)) {
848             records = Array.prototype.slice.apply(arguments);
849         }
850
851         var me = this,
852             i = 0,
853             length = records.length,
854             record;
855
856         for (; i < length; i++) {
857             record = me.createModel(records[i]);
858             // reassign the model in the array in case it wasn't created yet
859             records[i] = record;
860         }
861
862         me.insert(me.data.length, records);
863
864         return records;
865     },
866
867     /**
868      * Converts a literal to a model, if it's not a model already
869      * @private
870      * @param record {Ext.data.Model/Object} The record to create
871      * @return {Ext.data.Model}
872      */
873     createModel: function(record) {
874         if (!record.isModel) {
875             record = Ext.ModelManager.create(record, this.model);
876         }
877
878         return record;
879     },
880
881     /**
882      * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
883      * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter.
884      * Returning <tt>false</tt> aborts and exits the iteration.
885      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed.
886      * Defaults to the current {@link Ext.data.Model Record} in the iteration.
887      */
888     each: function(fn, scope) {
889         this.data.each(fn, scope);
890     },
891
892     /**
893      * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single
894      * 'datachanged' event after removal.
895      * @param {Ext.data.Model/Ext.data.Model[]} records The Ext.data.Model instance or array of instances to remove
896      */
897     remove: function(records, /* private */ isMove) {
898         if (!Ext.isArray(records)) {
899             records = [records];
900         }
901
902         /*
903          * Pass the isMove parameter if we know we're going to be re-inserting this record
904          */
905         isMove = isMove === true;
906         var me = this,
907             sync = false,
908             i = 0,
909             length = records.length,
910             isPhantom,
911             index,
912             record;
913
914         for (; i < length; i++) {
915             record = records[i];
916             index = me.data.indexOf(record);
917
918             if (me.snapshot) {
919                 me.snapshot.remove(record);
920             }
921
922             if (index > -1) {
923                 isPhantom = record.phantom === true;
924                 if (!isMove && !isPhantom) {
925                     // don't push phantom records onto removed
926                     me.removed.push(record);
927                 }
928
929                 record.unjoin(me);
930                 me.data.remove(record);
931                 sync = sync || !isPhantom;
932
933                 me.fireEvent('remove', me, record, index);
934             }
935         }
936
937         me.fireEvent('datachanged', me);
938         if (!isMove && me.autoSync && sync) {
939             me.sync();
940         }
941     },
942
943     /**
944      * Removes the model instance at the given index
945      * @param {Number} index The record index
946      */
947     removeAt: function(index) {
948         var record = this.getAt(index);
949
950         if (record) {
951             this.remove(record);
952         }
953     },
954
955     /**
956      * <p>Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
957      * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
958      * instances into the Store and calling an optional callback if required. Example usage:</p>
959      *
960 <pre><code>
961 store.load({
962     scope   : this,
963     callback: function(records, operation, success) {
964         //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
965         console.log(records);
966     }
967 });
968 </code></pre>
969      *
970      * <p>If the callback scope does not need to be set, a function can simply be passed:</p>
971      *
972 <pre><code>
973 store.load(function(records, operation, success) {
974     console.log('loaded records');
975 });
976 </code></pre>
977      *
978      * @param {Object/Function} options (Optional) config object, passed into the Ext.data.Operation object before loading.
979      */
980     load: function(options) {
981         var me = this;
982
983         options = options || {};
984
985         if (Ext.isFunction(options)) {
986             options = {
987                 callback: options
988             };
989         }
990
991         Ext.applyIf(options, {
992             groupers: me.groupers.items,
993             page: me.currentPage,
994             start: (me.currentPage - 1) * me.pageSize,
995             limit: me.pageSize,
996             addRecords: false
997         });
998
999         return me.callParent([options]);
1000     },
1001
1002     /**
1003      * @private
1004      * Called internally when a Proxy has completed a load request
1005      */
1006     onProxyLoad: function(operation) {
1007         var me = this,
1008             resultSet = operation.getResultSet(),
1009             records = operation.getRecords(),
1010             successful = operation.wasSuccessful();
1011
1012         if (resultSet) {
1013             me.totalCount = resultSet.total;
1014         }
1015
1016         if (successful) {
1017             me.loadRecords(records, operation);
1018         }
1019
1020         me.loading = false;
1021         me.fireEvent('load', me, records, successful);
1022
1023         //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not.
1024         //People are definitely using this so can't deprecate safely until 2.x
1025         me.fireEvent('read', me, records, operation.wasSuccessful());
1026
1027         //this is a callback that would have been passed to the 'read' function and is optional
1028         Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
1029     },
1030
1031     /**
1032      * Create any new records when a write is returned from the server.
1033      * @private
1034      * @param {Ext.data.Model[]} records The array of new records
1035      * @param {Ext.data.Operation} operation The operation that just completed
1036      * @param {Boolean} success True if the operation was successful
1037      */
1038     onCreateRecords: function(records, operation, success) {
1039         if (success) {
1040             var i = 0,
1041                 data = this.data,
1042                 snapshot = this.snapshot,
1043                 length = records.length,
1044                 originalRecords = operation.records,
1045                 record,
1046                 original,
1047                 index;
1048
1049             /*
1050              * Loop over each record returned from the server. Assume they are
1051              * returned in order of how they were sent. If we find a matching
1052              * record, replace it with the newly created one.
1053              */
1054             for (; i < length; ++i) {
1055                 record = records[i];
1056                 original = originalRecords[i];
1057                 if (original) {
1058                     index = data.indexOf(original);
1059                     if (index > -1) {
1060                         data.removeAt(index);
1061                         data.insert(index, record);
1062                     }
1063                     if (snapshot) {
1064                         index = snapshot.indexOf(original);
1065                         if (index > -1) {
1066                             snapshot.removeAt(index);
1067                             snapshot.insert(index, record);
1068                         }
1069                     }
1070                     record.phantom = false;
1071                     record.join(this);
1072                 }
1073             }
1074         }
1075     },
1076
1077     /**
1078      * Update any records when a write is returned from the server.
1079      * @private
1080      * @param {Ext.data.Model[]} records The array of updated records
1081      * @param {Ext.data.Operation} operation The operation that just completed
1082      * @param {Boolean} success True if the operation was successful
1083      */
1084     onUpdateRecords: function(records, operation, success){
1085         if (success) {
1086             var i = 0,
1087                 length = records.length,
1088                 data = this.data,
1089                 snapshot = this.snapshot,
1090                 record;
1091
1092             for (; i < length; ++i) {
1093                 record = records[i];
1094                 data.replace(record);
1095                 if (snapshot) {
1096                     snapshot.replace(record);
1097                 }
1098                 record.join(this);
1099             }
1100         }
1101     },
1102
1103     /**
1104      * Remove any records when a write is returned from the server.
1105      * @private
1106      * @param {Ext.data.Model[]} records The array of removed records
1107      * @param {Ext.data.Operation} operation The operation that just completed
1108      * @param {Boolean} success True if the operation was successful
1109      */
1110     onDestroyRecords: function(records, operation, success){
1111         if (success) {
1112             var me = this,
1113                 i = 0,
1114                 length = records.length,
1115                 data = me.data,
1116                 snapshot = me.snapshot,
1117                 record;
1118
1119             for (; i < length; ++i) {
1120                 record = records[i];
1121                 record.unjoin(me);
1122                 data.remove(record);
1123                 if (snapshot) {
1124                     snapshot.remove(record);
1125                 }
1126             }
1127             me.removed = [];
1128         }
1129     },
1130
1131     //inherit docs
1132     getNewRecords: function() {
1133         return this.data.filterBy(this.filterNew).items;
1134     },
1135
1136     //inherit docs
1137     getUpdatedRecords: function() {
1138         return this.data.filterBy(this.filterUpdated).items;
1139     },
1140
1141     /**
1142      * Filters the loaded set of records by a given set of filters.
1143      *
1144      * Filtering by single field:
1145      *
1146      *     store.filter("email", /\.com$/);
1147      *
1148      * Using multiple filters:
1149      *
1150      *     store.filter([
1151      *         {property: "email", value: /\.com$/},
1152      *         {filterFn: function(item) { return item.get("age") > 10; }}
1153      *     ]);
1154      *
1155      * Using Ext.util.Filter instances instead of config objects
1156      * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
1157      *
1158      *     store.filter([
1159      *         Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
1160      *         Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
1161      *     ]);
1162      *
1163      * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data. These are stored internally on the store,
1164      * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
1165      * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
1166      * pass in a property string
1167      * @param {String} value (optional) value to filter by (only if using a property string as the first argument)
1168      */
1169     filter: function(filters, value) {
1170         if (Ext.isString(filters)) {
1171             filters = {
1172                 property: filters,
1173                 value: value
1174             };
1175         }
1176
1177         var me = this,
1178             decoded = me.decodeFilters(filters),
1179             i = 0,
1180             doLocalSort = me.sortOnFilter && !me.remoteSort,
1181             length = decoded.length;
1182
1183         for (; i < length; i++) {
1184             me.filters.replace(decoded[i]);
1185         }
1186
1187         if (me.remoteFilter) {
1188             //the load function will pick up the new filters and request the filtered data from the proxy
1189             me.load();
1190         } else {
1191             /**
1192              * A pristine (unfiltered) collection of the records in this store. This is used to reinstate
1193              * records when a filter is removed or changed
1194              * @property snapshot
1195              * @type Ext.util.MixedCollection
1196              */
1197             if (me.filters.getCount()) {
1198                 me.snapshot = me.snapshot || me.data.clone();
1199                 me.data = me.data.filter(me.filters.items);
1200
1201                 if (doLocalSort) {
1202                     me.sort();
1203                 }
1204                 // fire datachanged event if it hasn't already been fired by doSort
1205                 if (!doLocalSort || me.sorters.length < 1) {
1206                     me.fireEvent('datachanged', me);
1207                 }
1208             }
1209         }
1210     },
1211
1212     /**
1213      * Revert to a view of the Record cache with no filtering applied.
1214      * @param {Boolean} suppressEvent If <tt>true</tt> the filter is cleared silently without firing the
1215      * {@link #datachanged} event.
1216      */
1217     clearFilter: function(suppressEvent) {
1218         var me = this;
1219
1220         me.filters.clear();
1221
1222         if (me.remoteFilter) {
1223             me.load();
1224         } else if (me.isFiltered()) {
1225             me.data = me.snapshot.clone();
1226             delete me.snapshot;
1227
1228             if (suppressEvent !== true) {
1229                 me.fireEvent('datachanged', me);
1230             }
1231         }
1232     },
1233
1234     /**
1235      * Returns true if this store is currently filtered
1236      * @return {Boolean}
1237      */
1238     isFiltered: function() {
1239         var snapshot = this.snapshot;
1240         return !! snapshot && snapshot !== this.data;
1241     },
1242
1243     /**
1244      * Filter by a function. The specified function will be called for each
1245      * Record in this Store. If the function returns <tt>true</tt> the Record is included,
1246      * otherwise it is filtered out.
1247      * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1248      * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1249      * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1250      * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1251      * </ul>
1252      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1253      */
1254     filterBy: function(fn, scope) {
1255         var me = this;
1256
1257         me.snapshot = me.snapshot || me.data.clone();
1258         me.data = me.queryBy(fn, scope || me);
1259         me.fireEvent('datachanged', me);
1260     },
1261
1262     /**
1263      * Query the cached records in this Store using a filtering function. The specified function
1264      * will be called with each record in this Store. If the function returns <tt>true</tt> the record is
1265      * included in the results.
1266      * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1267      * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1268      * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1269      * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1270      * </ul>
1271      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1272      * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records
1273      **/
1274     queryBy: function(fn, scope) {
1275         var me = this,
1276         data = me.snapshot || me.data;
1277         return data.filterBy(fn, scope || me);
1278     },
1279
1280     /**
1281      * Loads an array of data straight into the Store.
1282      * 
1283      * Using this method is great if the data is in the correct format already (e.g. it doesn't need to be
1284      * processed by a reader). If your data requires processing to decode the data structure, use a
1285      * {@link Ext.data.proxy.Memory MemoryProxy} instead.
1286      * 
1287      * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will be cast
1288      * into model instances.
1289      * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
1290      * to remove the old ones first.
1291      */
1292     loadData: function(data, append) {
1293         var model = this.model,
1294             length = data.length,
1295             newData = [],
1296             i,
1297             record;
1298
1299         //make sure each data element is an Ext.data.Model instance
1300         for (i = 0; i < length; i++) {
1301             record = data[i];
1302
1303             if (!(record instanceof Ext.data.Model)) {
1304                 record = Ext.ModelManager.create(record, model);
1305             }
1306             newData.push(record);
1307         }
1308
1309         this.loadRecords(newData, {addRecords: append});
1310     },
1311
1312
1313     /**
1314      * Loads data via the bound Proxy's reader
1315      *
1316      * Use this method if you are attempting to load data and want to utilize the configured data reader.
1317      *
1318      * @param {Object[]} data The full JSON object you'd like to load into the Data store.
1319      * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
1320      * to remove the old ones first.
1321      */
1322     loadRawData : function(data, append) {
1323          var me      = this,
1324              result  = me.proxy.reader.read(data),
1325              records = result.records;
1326
1327          if (result.success) {
1328              me.loadRecords(records, { addRecords: append });
1329              me.fireEvent('load', me, records, true);
1330          }
1331      },
1332
1333
1334     /**
1335      * Loads an array of {@link Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually
1336      * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead
1337      * @param {Ext.data.Model[]} records The array of records to load
1338      * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first
1339      */
1340     loadRecords: function(records, options) {
1341         var me     = this,
1342             i      = 0,
1343             length = records.length;
1344
1345         options = options || {};
1346
1347
1348         if (!options.addRecords) {
1349             delete me.snapshot;
1350             me.clearData();
1351         }
1352
1353         me.data.addAll(records);
1354
1355         //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately.
1356         for (; i < length; i++) {
1357             if (options.start !== undefined) {
1358                 records[i].index = options.start + i;
1359
1360             }
1361             records[i].join(me);
1362         }
1363
1364         /*
1365          * this rather inelegant suspension and resumption of events is required because both the filter and sort functions
1366          * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first
1367          * datachanged event is fired by the call to this.add, above.
1368          */
1369         me.suspendEvents();
1370
1371         if (me.filterOnLoad && !me.remoteFilter) {
1372             me.filter();
1373         }
1374
1375         if (me.sortOnLoad && !me.remoteSort) {
1376             me.sort();
1377         }
1378
1379         me.resumeEvents();
1380         me.fireEvent('datachanged', me, records);
1381     },
1382
1383     // PAGING METHODS
1384     /**
1385      * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
1386      * load operation, passing in calculated 'start' and 'limit' params
1387      * @param {Number} page The number of the page to load
1388      * @param {Object} options See options for {@link #load}
1389      */
1390     loadPage: function(page, options) {
1391         var me = this;
1392         options = Ext.apply({}, options);
1393
1394         me.currentPage = page;
1395
1396         me.read(Ext.applyIf(options, {
1397             page: page,
1398             start: (page - 1) * me.pageSize,
1399             limit: me.pageSize,
1400             addRecords: !me.clearOnPageLoad
1401         }));
1402     },
1403
1404     /**
1405      * Loads the next 'page' in the current data set
1406      * @param {Object} options See options for {@link #load}
1407      */
1408     nextPage: function(options) {
1409         this.loadPage(this.currentPage + 1, options);
1410     },
1411
1412     /**
1413      * Loads the previous 'page' in the current data set
1414      * @param {Object} options See options for {@link #load}
1415      */
1416     previousPage: function(options) {
1417         this.loadPage(this.currentPage - 1, options);
1418     },
1419
1420     // private
1421     clearData: function() {
1422         var me = this;
1423         me.data.each(function(record) {
1424             record.unjoin(me);
1425         });
1426
1427         me.data.clear();
1428     },
1429
1430     // Buffering
1431     /**
1432      * Prefetches data into the store using its configured {@link #proxy}.
1433      * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
1434      * See {@link #load}
1435      */
1436     prefetch: function(options) {
1437         var me = this,
1438             operation,
1439             requestId = me.getRequestId();
1440
1441         options = options || {};
1442
1443         Ext.applyIf(options, {
1444             action : 'read',
1445             filters: me.filters.items,
1446             sorters: me.sorters.items,
1447             requestId: requestId
1448         });
1449         me.pendingRequests.push(requestId);
1450
1451         operation = Ext.create('Ext.data.Operation', options);
1452
1453         // HACK to implement loadMask support.
1454         //if (operation.blocking) {
1455         //    me.fireEvent('beforeload', me, operation);
1456         //}
1457         if (me.fireEvent('beforeprefetch', me, operation) !== false) {
1458             me.loading = true;
1459             me.proxy.read(operation, me.onProxyPrefetch, me);
1460         }
1461
1462         return me;
1463     },
1464
1465     /**
1466      * Prefetches a page of data.
1467      * @param {Number} page The page to prefetch
1468      * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
1469      * See {@link #load}
1470      */
1471     prefetchPage: function(page, options) {
1472         var me = this,
1473             pageSize = me.pageSize,
1474             start = (page - 1) * me.pageSize,
1475             end = start + pageSize;
1476
1477         // Currently not requesting this page and range isn't already satisified
1478         if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) {
1479             options = options || {};
1480             me.pagesRequested.push(page);
1481             Ext.applyIf(options, {
1482                 page : page,
1483                 start: start,
1484                 limit: pageSize,
1485                 callback: me.onWaitForGuarantee,
1486                 scope: me
1487             });
1488
1489             me.prefetch(options);
1490         }
1491
1492     },
1493
1494     /**
1495      * Returns a unique requestId to track requests.
1496      * @private
1497      */
1498     getRequestId: function() {
1499         this.requestSeed = this.requestSeed || 1;
1500         return this.requestSeed++;
1501     },
1502
1503     /**
1504      * Called after the configured proxy completes a prefetch operation.
1505      * @private
1506      * @param {Ext.data.Operation} operation The operation that completed
1507      */
1508     onProxyPrefetch: function(operation) {
1509         var me         = this,
1510             resultSet  = operation.getResultSet(),
1511             records    = operation.getRecords(),
1512
1513             successful = operation.wasSuccessful();
1514
1515         if (resultSet) {
1516             me.totalCount = resultSet.total;
1517             me.fireEvent('totalcountchange', me.totalCount);
1518         }
1519
1520         if (successful) {
1521             me.cacheRecords(records, operation);
1522         }
1523         Ext.Array.remove(me.pendingRequests, operation.requestId);
1524         if (operation.page) {
1525             Ext.Array.remove(me.pagesRequested, operation.page);
1526         }
1527
1528         me.loading = false;
1529         me.fireEvent('prefetch', me, records, successful, operation);
1530
1531         // HACK to support loadMask
1532         if (operation.blocking) {
1533             me.fireEvent('load', me, records, successful);
1534         }
1535
1536         //this is a callback that would have been passed to the 'read' function and is optional
1537         Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
1538     },
1539
1540     /**
1541      * Caches the records in the prefetch and stripes them with their server-side
1542      * index.
1543      * @private
1544      * @param {Ext.data.Model[]} records The records to cache
1545      * @param {Ext.data.Operation} The associated operation
1546      */
1547     cacheRecords: function(records, operation) {
1548         var me     = this,
1549             i      = 0,
1550             length = records.length,
1551             start  = operation ? operation.start : 0;
1552
1553         if (!Ext.isDefined(me.totalCount)) {
1554             me.totalCount = records.length;
1555             me.fireEvent('totalcountchange', me.totalCount);
1556         }
1557
1558         for (; i < length; i++) {
1559             // this is the true index, not the viewIndex
1560             records[i].index = start + i;
1561         }
1562
1563         me.prefetchData.addAll(records);
1564         if (me.purgePageCount) {
1565             me.purgeRecords();
1566         }
1567
1568     },
1569
1570
1571     /**
1572      * Purge the least recently used records in the prefetch if the purgeCount
1573      * has been exceeded.
1574      */
1575     purgeRecords: function() {
1576         var me = this,
1577             prefetchCount = me.prefetchData.getCount(),
1578             purgeCount = me.purgePageCount * me.pageSize,
1579             numRecordsToPurge = prefetchCount - purgeCount - 1,
1580             i = 0;
1581
1582         for (; i <= numRecordsToPurge; i++) {
1583             me.prefetchData.removeAt(0);
1584         }
1585     },
1586
1587     /**
1588      * Determines if the range has already been satisfied in the prefetchData.
1589      * @private
1590      * @param {Number} start The start index
1591      * @param {Number} end The end index in the range
1592      */
1593     rangeSatisfied: function(start, end) {
1594         var me = this,
1595             i = start,
1596             satisfied = true;
1597
1598         for (; i < end; i++) {
1599             if (!me.prefetchData.getByKey(i)) {
1600                 satisfied = false;
1601                 //<debug>
1602                 if (end - i > me.pageSize) {
1603                     Ext.Error.raise("A single page prefetch could never satisfy this request.");
1604                 }
1605                 //</debug>
1606                 break;
1607             }
1608         }
1609         return satisfied;
1610     },
1611
1612     /**
1613      * Determines the page from a record index
1614      * @param {Number} index The record index
1615      * @return {Number} The page the record belongs to
1616      */
1617     getPageFromRecordIndex: function(index) {
1618         return Math.floor(index / this.pageSize) + 1;
1619     },
1620
1621     /**
1622      * Handles a guaranteed range being loaded
1623      * @private
1624      */
1625     onGuaranteedRange: function() {
1626         var me = this,
1627             totalCount = me.getTotalCount(),
1628             start = me.requestStart,
1629             end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
1630             range = [],
1631             record,
1632             i = start;
1633
1634         end = Math.max(0, end);
1635
1636         //<debug>
1637         if (start > end) {
1638             Ext.log({
1639                 level: 'warn',
1640                 msg: 'Start (' + start + ') was greater than end (' + end +
1641                     ') for the range of records requested (' + me.requestStart + '-' +
1642                     me.requestEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')
1643             });
1644         }
1645         //</debug>
1646
1647         if (start !== me.guaranteedStart && end !== me.guaranteedEnd) {
1648             me.guaranteedStart = start;
1649             me.guaranteedEnd = end;
1650
1651             for (; i <= end; i++) {
1652                 record = me.prefetchData.getByKey(i);
1653                 //<debug>
1654 //                if (!record) {
1655 //                    Ext.log('Record with key "' + i + '" was not found and store said it was guaranteed');
1656 //                }
1657                 //</debug>
1658                 if (record) {
1659                     range.push(record);
1660                 }
1661             }
1662             me.fireEvent('guaranteedrange', range, start, end);
1663             if (me.cb) {
1664                 me.cb.call(me.scope || me, range);
1665             }
1666         }
1667
1668         me.unmask();
1669     },
1670
1671     // hack to support loadmask
1672     mask: function() {
1673         this.masked = true;
1674         this.fireEvent('beforeload');
1675     },
1676
1677     // hack to support loadmask
1678     unmask: function() {
1679         if (this.masked) {
1680             this.fireEvent('load');
1681         }
1682     },
1683
1684     /**
1685      * Returns the number of pending requests out.
1686      */
1687     hasPendingRequests: function() {
1688         return this.pendingRequests.length;
1689     },
1690
1691
1692     // wait until all requests finish, until guaranteeing the range.
1693     onWaitForGuarantee: function() {
1694         if (!this.hasPendingRequests()) {
1695             this.onGuaranteedRange();
1696         }
1697     },
1698
1699     /**
1700      * Guarantee a specific range, this will load the store with a range (that
1701      * must be the pageSize or smaller) and take care of any loading that may
1702      * be necessary.
1703      */
1704     guaranteeRange: function(start, end, cb, scope) {
1705         //<debug>
1706         if (start && end) {
1707             if (end - start > this.pageSize) {
1708                 Ext.Error.raise({
1709                     start: start,
1710                     end: end,
1711                     pageSize: this.pageSize,
1712                     msg: "Requested a bigger range than the specified pageSize"
1713                 });
1714             }
1715         }
1716         //</debug>
1717
1718         end = (end > this.totalCount) ? this.totalCount - 1 : end;
1719
1720         var me = this,
1721             i = start,
1722             prefetchData = me.prefetchData,
1723             range = [],
1724             startLoaded = !!prefetchData.getByKey(start),
1725             endLoaded = !!prefetchData.getByKey(end),
1726             startPage = me.getPageFromRecordIndex(start),
1727             endPage = me.getPageFromRecordIndex(end);
1728
1729         me.cb = cb;
1730         me.scope = scope;
1731
1732         me.requestStart = start;
1733         me.requestEnd = end;
1734         // neither beginning or end are loaded
1735         if (!startLoaded || !endLoaded) {
1736             // same page, lets load it
1737             if (startPage === endPage) {
1738                 me.mask();
1739                 me.prefetchPage(startPage, {
1740                     //blocking: true,
1741                     callback: me.onWaitForGuarantee,
1742                     scope: me
1743                 });
1744             // need to load two pages
1745             } else {
1746                 me.mask();
1747                 me.prefetchPage(startPage, {
1748                     //blocking: true,
1749                     callback: me.onWaitForGuarantee,
1750                     scope: me
1751                 });
1752                 me.prefetchPage(endPage, {
1753                     //blocking: true,
1754                     callback: me.onWaitForGuarantee,
1755                     scope: me
1756                 });
1757             }
1758         // Request was already satisfied via the prefetch
1759         } else {
1760             me.onGuaranteedRange();
1761         }
1762     },
1763
1764     // because prefetchData is stored by index
1765     // this invalidates all of the prefetchedData
1766     sort: function() {
1767         var me = this,
1768             prefetchData = me.prefetchData,
1769             sorters,
1770             start,
1771             end,
1772             range;
1773
1774         if (me.buffered) {
1775             if (me.remoteSort) {
1776                 prefetchData.clear();
1777                 me.callParent(arguments);
1778             } else {
1779                 sorters = me.getSorters();
1780                 start = me.guaranteedStart;
1781                 end = me.guaranteedEnd;
1782
1783                 if (sorters.length) {
1784                     prefetchData.sort(sorters);
1785                     range = prefetchData.getRange();
1786                     prefetchData.clear();
1787                     me.cacheRecords(range);
1788                     delete me.guaranteedStart;
1789                     delete me.guaranteedEnd;
1790                     me.guaranteeRange(start, end);
1791                 }
1792                 me.callParent(arguments);
1793             }
1794         } else {
1795             me.callParent(arguments);
1796         }
1797     },
1798
1799     // overriden to provide striping of the indexes as sorting occurs.
1800     // this cannot be done inside of sort because datachanged has already
1801     // fired and will trigger a repaint of the bound view.
1802     doSort: function(sorterFn) {
1803         var me = this;
1804         if (me.remoteSort) {
1805             //the load function will pick up the new sorters and request the sorted data from the proxy
1806             me.load();
1807         } else {
1808             me.data.sortBy(sorterFn);
1809             if (!me.buffered) {
1810                 var range = me.getRange(),
1811                     ln = range.length,
1812                     i  = 0;
1813                 for (; i < ln; i++) {
1814                     range[i].index = i;
1815                 }
1816             }
1817             me.fireEvent('datachanged', me);
1818         }
1819     },
1820
1821     /**
1822      * Finds the index of the first matching Record in this store by a specific field value.
1823      * @param {String} fieldName The name of the Record field to test.
1824      * @param {String/RegExp} value Either a string that the field value
1825      * should begin with, or a RegExp to test against the field.
1826      * @param {Number} startIndex (optional) The index to start searching at
1827      * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
1828      * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
1829      * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
1830      * @return {Number} The matched index or -1
1831      */
1832     find: function(property, value, start, anyMatch, caseSensitive, exactMatch) {
1833         var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch);
1834         return fn ? this.data.findIndexBy(fn, null, start) : -1;
1835     },
1836
1837     /**
1838      * Finds the first matching Record in this store by a specific field value.
1839      * @param {String} fieldName The name of the Record field to test.
1840      * @param {String/RegExp} value Either a string that the field value
1841      * should begin with, or a RegExp to test against the field.
1842      * @param {Number} startIndex (optional) The index to start searching at
1843      * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
1844      * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
1845      * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
1846      * @return {Ext.data.Model} The matched record or null
1847      */
1848     findRecord: function() {
1849         var me = this,
1850             index = me.find.apply(me, arguments);
1851         return index !== -1 ? me.getAt(index) : null;
1852     },
1853
1854     /**
1855      * @private
1856      * Returns a filter function used to test a the given property's value. Defers most of the work to
1857      * Ext.util.MixedCollection's createValueMatcher function
1858      * @param {String} property The property to create the filter function for
1859      * @param {String/RegExp} value The string/regex to compare the property value to
1860      * @param {Boolean} [anyMatch=false] True if we don't care if the filter value is not the full value.
1861      * @param {Boolean} [caseSensitive=false] True to create a case-sensitive regex.
1862      * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters added to the regex).
1863      * Ignored if anyMatch is true.
1864      */
1865     createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
1866         if (Ext.isEmpty(value)) {
1867             return false;
1868         }
1869         value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
1870         return function(r) {
1871             return value.test(r.data[property]);
1872         };
1873     },
1874
1875     /**
1876      * Finds the index of the first matching Record in this store by a specific field value.
1877      * @param {String} fieldName The name of the Record field to test.
1878      * @param {Object} value The value to match the field against.
1879      * @param {Number} startIndex (optional) The index to start searching at
1880      * @return {Number} The matched index or -1
1881      */
1882     findExact: function(property, value, start) {
1883         return this.data.findIndexBy(function(rec) {
1884             return rec.get(property) == value;
1885         },
1886         this, start);
1887     },
1888
1889     /**
1890      * Find the index of the first matching Record in this Store by a function.
1891      * If the function returns <tt>true</tt> it is considered a match.
1892      * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1893      * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1894      * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1895      * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1896      * </ul>
1897      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1898      * @param {Number} startIndex (optional) The index to start searching at
1899      * @return {Number} The matched index or -1
1900      */
1901     findBy: function(fn, scope, start) {
1902         return this.data.findIndexBy(fn, scope, start);
1903     },
1904
1905     /**
1906      * Collects unique values for a particular dataIndex from this store.
1907      * @param {String} dataIndex The property to collect
1908      * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values
1909      * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered
1910      * @return {Object[]} An array of the unique values
1911      **/
1912     collect: function(dataIndex, allowNull, bypassFilter) {
1913         var me = this,
1914             data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
1915
1916         return data.collect(dataIndex, 'data', allowNull);
1917     },
1918
1919     /**
1920      * Gets the number of cached records.
1921      * <p>If using paging, this may not be the total size of the dataset. If the data object
1922      * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns
1923      * the dataset size.  <b>Note</b>: see the Important note in {@link #load}.</p>
1924      * @return {Number} The number of Records in the Store's cache.
1925      */
1926     getCount: function() {
1927         return this.data.length || 0;
1928     },
1929
1930     /**
1931      * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy}
1932      * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the
1933      * number of records loaded into the Store at the moment, getTotalCount returns the number of records that
1934      * could be loaded into the Store if the Store contained all data
1935      * @return {Number} The total number of Model instances available via the Proxy
1936      */
1937     getTotalCount: function() {
1938         return this.totalCount;
1939     },
1940
1941     /**
1942      * Get the Record at the specified index.
1943      * @param {Number} index The index of the Record to find.
1944      * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found.
1945      */
1946     getAt: function(index) {
1947         return this.data.getAt(index);
1948     },
1949
1950     /**
1951      * Returns a range of Records between specified indices.
1952      * @param {Number} [startIndex=0] The starting index
1953      * @param {Number} [endIndex] The ending index. Defaults to the last Record in the Store.
1954      * @return {Ext.data.Model[]} An array of Records
1955      */
1956     getRange: function(start, end) {
1957         return this.data.getRange(start, end);
1958     },
1959
1960     /**
1961      * Get the Record with the specified id.
1962      * @param {String} id The id of the Record to find.
1963      * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
1964      */
1965     getById: function(id) {
1966         return (this.snapshot || this.data).findBy(function(record) {
1967             return record.getId() === id;
1968         });
1969     },
1970
1971     /**
1972      * Get the index within the cache of the passed Record.
1973      * @param {Ext.data.Model} record The Ext.data.Model object to find.
1974      * @return {Number} The index of the passed Record. Returns -1 if not found.
1975      */
1976     indexOf: function(record) {
1977         return this.data.indexOf(record);
1978     },
1979
1980
1981     /**
1982      * Get the index within the entire dataset. From 0 to the totalCount.
1983      * @param {Ext.data.Model} record The Ext.data.Model object to find.
1984      * @return {Number} The index of the passed Record. Returns -1 if not found.
1985      */
1986     indexOfTotal: function(record) {
1987         var index = record.index;
1988         if (index || index === 0) {
1989             return index;
1990         }
1991         return this.indexOf(record);
1992     },
1993
1994     /**
1995      * Get the index within the cache of the Record with the passed id.
1996      * @param {String} id The id of the Record to find.
1997      * @return {Number} The index of the Record. Returns -1 if not found.
1998      */
1999     indexOfId: function(id) {
2000         return this.indexOf(this.getById(id));
2001     },
2002
2003     /**
2004      * Remove all items from the store.
2005      * @param {Boolean} silent Prevent the `clear` event from being fired.
2006      */
2007     removeAll: function(silent) {
2008         var me = this;
2009
2010         me.clearData();
2011         if (me.snapshot) {
2012             me.snapshot.clear();
2013         }
2014         if (silent !== true) {
2015             me.fireEvent('clear', me);
2016         }
2017     },
2018
2019     /*
2020      * Aggregation methods
2021      */
2022
2023     /**
2024      * Convenience function for getting the first model instance in the store
2025      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2026      * in the store. The value returned will be an object literal with the key being the group
2027      * name and the first record being the value. The grouped parameter is only honored if
2028      * the store has a groupField.
2029      * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
2030      */
2031     first: function(grouped) {
2032         var me = this;
2033
2034         if (grouped && me.isGrouped()) {
2035             return me.aggregate(function(records) {
2036                 return records.length ? records[0] : undefined;
2037             }, me, true);
2038         } else {
2039             return me.data.first();
2040         }
2041     },
2042
2043     /**
2044      * Convenience function for getting the last model instance in the store
2045      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2046      * in the store. The value returned will be an object literal with the key being the group
2047      * name and the last record being the value. The grouped parameter is only honored if
2048      * the store has a groupField.
2049      * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
2050      */
2051     last: function(grouped) {
2052         var me = this;
2053
2054         if (grouped && me.isGrouped()) {
2055             return me.aggregate(function(records) {
2056                 var len = records.length;
2057                 return len ? records[len - 1] : undefined;
2058             }, me, true);
2059         } else {
2060             return me.data.last();
2061         }
2062     },
2063
2064     /**
2065      * Sums the value of <tt>property</tt> for each {@link Ext.data.Model record} between <tt>start</tt>
2066      * and <tt>end</tt> and returns the result.
2067      * @param {String} field A field in each record
2068      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2069      * in the store. The value returned will be an object literal with the key being the group
2070      * name and the sum for that group being the value. The grouped parameter is only honored if
2071      * the store has a groupField.
2072      * @return {Number} The sum
2073      */
2074     sum: function(field, grouped) {
2075         var me = this;
2076
2077         if (grouped && me.isGrouped()) {
2078             return me.aggregate(me.getSum, me, true, [field]);
2079         } else {
2080             return me.getSum(me.data.items, field);
2081         }
2082     },
2083
2084     // @private, see sum
2085     getSum: function(records, field) {
2086         var total = 0,
2087             i = 0,
2088             len = records.length;
2089
2090         for (; i < len; ++i) {
2091             total += records[i].get(field);
2092         }
2093
2094         return total;
2095     },
2096
2097     /**
2098      * Gets the count of items in the store.
2099      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2100      * in the store. The value returned will be an object literal with the key being the group
2101      * name and the count for each group being the value. The grouped parameter is only honored if
2102      * the store has a groupField.
2103      * @return {Number} the count
2104      */
2105     count: function(grouped) {
2106         var me = this;
2107
2108         if (grouped && me.isGrouped()) {
2109             return me.aggregate(function(records) {
2110                 return records.length;
2111             }, me, true);
2112         } else {
2113             return me.getCount();
2114         }
2115     },
2116
2117     /**
2118      * Gets the minimum value in the store.
2119      * @param {String} field The field in each record
2120      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2121      * in the store. The value returned will be an object literal with the key being the group
2122      * name and the minimum in the group being the value. The grouped parameter is only honored if
2123      * the store has a groupField.
2124      * @return {Object} The minimum value, if no items exist, undefined.
2125      */
2126     min: function(field, grouped) {
2127         var me = this;
2128
2129         if (grouped && me.isGrouped()) {
2130             return me.aggregate(me.getMin, me, true, [field]);
2131         } else {
2132             return me.getMin(me.data.items, field);
2133         }
2134     },
2135
2136     // @private, see min
2137     getMin: function(records, field){
2138         var i = 1,
2139             len = records.length,
2140             value, min;
2141
2142         if (len > 0) {
2143             min = records[0].get(field);
2144         }
2145
2146         for (; i < len; ++i) {
2147             value = records[i].get(field);
2148             if (value < min) {
2149                 min = value;
2150             }
2151         }
2152         return min;
2153     },
2154
2155     /**
2156      * Gets the maximum value in the store.
2157      * @param {String} field The field in each record
2158      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2159      * in the store. The value returned will be an object literal with the key being the group
2160      * name and the maximum in the group being the value. The grouped parameter is only honored if
2161      * the store has a groupField.
2162      * @return {Object} The maximum value, if no items exist, undefined.
2163      */
2164     max: function(field, grouped) {
2165         var me = this;
2166
2167         if (grouped && me.isGrouped()) {
2168             return me.aggregate(me.getMax, me, true, [field]);
2169         } else {
2170             return me.getMax(me.data.items, field);
2171         }
2172     },
2173
2174     // @private, see max
2175     getMax: function(records, field) {
2176         var i = 1,
2177             len = records.length,
2178             value,
2179             max;
2180
2181         if (len > 0) {
2182             max = records[0].get(field);
2183         }
2184
2185         for (; i < len; ++i) {
2186             value = records[i].get(field);
2187             if (value > max) {
2188                 max = value;
2189             }
2190         }
2191         return max;
2192     },
2193
2194     /**
2195      * Gets the average value in the store.
2196      * @param {String} field The field in each record
2197      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2198      * in the store. The value returned will be an object literal with the key being the group
2199      * name and the group average being the value. The grouped parameter is only honored if
2200      * the store has a groupField.
2201      * @return {Object} The average value, if no items exist, 0.
2202      */
2203     average: function(field, grouped) {
2204         var me = this;
2205         if (grouped && me.isGrouped()) {
2206             return me.aggregate(me.getAverage, me, true, [field]);
2207         } else {
2208             return me.getAverage(me.data.items, field);
2209         }
2210     },
2211
2212     // @private, see average
2213     getAverage: function(records, field) {
2214         var i = 0,
2215             len = records.length,
2216             sum = 0;
2217
2218         if (records.length > 0) {
2219             for (; i < len; ++i) {
2220                 sum += records[i].get(field);
2221             }
2222             return sum / len;
2223         }
2224         return 0;
2225     },
2226
2227     /**
2228      * Runs the aggregate function for all the records in the store.
2229      * @param {Function} fn The function to execute. The function is called with a single parameter,
2230      * an array of records for that group.
2231      * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store.
2232      * @param {Boolean} grouped (Optional) True to perform the operation for each group
2233      * in the store. The value returned will be an object literal with the key being the group
2234      * name and the group average being the value. The grouped parameter is only honored if
2235      * the store has a groupField.
2236      * @param {Array} args (optional) Any arguments to append to the function call
2237      * @return {Object} An object literal with the group names and their appropriate values.
2238      */
2239     aggregate: function(fn, scope, grouped, args) {
2240         args = args || [];
2241         if (grouped && this.isGrouped()) {
2242             var groups = this.getGroups(),
2243                 i = 0,
2244                 len = groups.length,
2245                 out = {},
2246                 group;
2247
2248             for (; i < len; ++i) {
2249                 group = groups[i];
2250                 out[group.name] = fn.apply(scope || this, [group.children].concat(args));
2251             }
2252             return out;
2253         } else {
2254             return fn.apply(scope || this, [this.data.items].concat(args));
2255         }
2256     }
2257 }, function() {
2258     // A dummy empty store with a fieldless Model defined in it.
2259     // Just for binding to Views which are instantiated with no Store defined.
2260     // They will be able to run and render fine, and be bound to a generated Store later.
2261     Ext.regStore('ext-empty-store', {fields: [], proxy: 'proxy'});
2262 });
2263