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