1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('models/record');
  9 
 10 /**
 11   @class
 12 
 13 
 14   The Store is where you can find all of your dataHashes. Stores can be
 15   chained for editing purposes and committed back one chain level at a time
 16   all the way back to a persistent data source.
 17 
 18   Every application you create should generally have its own store objects.
 19   Once you create the store, you will rarely need to work with the store
 20   directly except to retrieve records and collections.
 21 
 22   Internally, the store will keep track of changes to your json data hashes
 23   and manage syncing those changes with your data source.  A data source may
 24   be a server, local storage, or any other persistent code.
 25 
 26   @extends SC.Object
 27   @since SproutCore 1.0
 28 */
 29 SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
 30 
 31   /**
 32     An (optional) name of the store, which can be useful during debugging,
 33     especially if you have multiple nested stores.
 34 
 35     @type String
 36   */
 37   name: null,
 38 
 39   /**
 40     An array of all the chained stores that current rely on the receiver
 41     store.
 42 
 43     @type Array
 44   */
 45   nestedStores: null,
 46 
 47   /**
 48     The data source is the persistent storage that will provide data to the
 49     store and save changes.  You normally will set your data source when you
 50     first create your store in your application.
 51 
 52     @type SC.DataSource
 53   */
 54   dataSource: null,
 55 
 56   /**
 57     This type of store is not nested.
 58 
 59     @default NO
 60     @type Boolean
 61   */
 62   isNested: NO,
 63 
 64   /**
 65     This type of store is not nested.
 66 
 67     @default NO
 68     @type Boolean
 69   */
 70   commitRecordsAutomatically: NO,
 71 
 72   // ..........................................................
 73   // DATA SOURCE SUPPORT
 74   //
 75 
 76   /**
 77     Convenience method.  Sets the current data source to the passed property.
 78     This will also set the store property on the dataSource to the receiver.
 79 
 80     If you are using this from the `core.js` method of your app, you may need to
 81     just pass a string naming your data source class.  If this is the case,
 82     then your data source will be instantiated the first time it is requested.
 83 
 84     @param {SC.DataSource|String} dataSource the data source
 85     @returns {SC.Store} receiver
 86   */
 87   from: function(dataSource) {
 88     this.set('dataSource', dataSource);
 89     return this ;
 90   },
 91 
 92   // lazily convert data source to real object
 93   _getDataSource: function() {
 94     var ret = this.get('dataSource');
 95     if (typeof ret === SC.T_STRING) {
 96       ret = SC.requiredObjectForPropertyPath(ret);
 97       if (ret.isClass) ret = ret.create();
 98       this.set('dataSource', ret);
 99     }
100     return ret;
101   },
102 
103   /**
104     Convenience method.  Creates a `CascadeDataSource` with the passed
105     data source arguments and sets the `CascadeDataSource` as the data source
106     for the receiver.
107 
108     @param {SC.DataSource...} dataSource one or more data source arguments
109     @returns {SC.Store} receiver
110   */
111   cascade: function(dataSource) {
112     var dataSources;
113 
114     // Fast arguments access.
115     // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
116     dataSources = new Array(arguments.length); //  SC.A(arguments)
117     for (var i = 0, len = dataSources.length; i < len; i++) { dataSources[i] = arguments[i]; }
118 
119     dataSource = SC.CascadeDataSource.create({
120       dataSources: dataSources
121     });
122     return this.from(dataSource);
123   },
124 
125   // ..........................................................
126   // STORE CHAINING
127   //
128 
129   /**
130     Returns a new nested store instance that can be used to buffer changes
131     until you are ready to commit them.  When you are ready to commit your
132     changes, call `commitChanges()` or `destroyChanges()` and then `destroy()`
133     when you are finished with the chained store altogether.
134 
135         store = MyApp.store.chain();
136         .. edit edit edit
137         store.commitChanges().destroy();
138 
139     @param {Hash} attrs optional attributes to set on new store
140     @param {Class} newStoreClass optional the class of the newly-created nested store (defaults to SC.NestedStore)
141     @returns {SC.NestedStore} new nested store chained to receiver
142   */
143   chain: function(attrs, newStoreClass) {
144     if (!attrs) attrs = {};
145     attrs.parentStore = this;
146 
147     if (newStoreClass) {
148       // Ensure the passed-in class is a type of nested store.
149       if (SC.typeOf(newStoreClass) !== 'class') throw new Error("%@ is not a valid class".fmt(newStoreClass));
150       if (!SC.kindOf(newStoreClass, SC.NestedStore)) throw new Error("%@ is not a type of SC.NestedStore".fmt(newStoreClass));
151     }
152     else {
153       newStoreClass = SC.NestedStore;
154     }
155 
156     // Replicate parent records references
157     attrs.childRecords = this.childRecords ? SC.clone(this.childRecords) : {};
158     attrs.parentRecords = this.parentRecords ? SC.clone(this.parentRecords) : {};
159 
160     var ret    = newStoreClass.create(attrs),
161         nested = this.nestedStores;
162 
163     if (!nested) nested = this.nestedStores = [];
164     nested.push(ret);
165     return ret ;
166   },
167 
168   /**
169     Creates an autonomous nested store that is connected to the data source.
170 
171     Use this kind of nested store to ensure that all records that are committed into the main store
172     are first of all committed to the server.
173 
174     For example,
175 
176         nestedStore = store.chainAutonomousStore();
177 
178         // ... commit all changes from the nested store to the remote data store
179         nestedStore.commitRecords();
180 
181         // or commit the changes of a nested store's record to the remote data store ...
182         nestedRecord.commitRecord();
183 
184     ## Resolving nested store commits with the main store
185 
186     When the committed changes are deemed successful (either by observing the status of the modified
187     record(s) or by using callbacks with commitRecord/commitRecords), the changes can be passed back
188     to the main store.
189 
190     In the case that the commits are all successful, simply commit to the main store using
191     `commitSuccessfulChanges`. Note, that using `commitSuccessfulChanges` rather than the standard
192     `commitChanges` ensures that only clean changes propagate back to the main store.
193 
194     For example,
195 
196         nestedStore.commitSuccessfulChanges();
197 
198     In the case that some or all of the commits fail, you can still use `commitSuccessfulChanges` to
199     update only those commits that have succeeded in the main store or wait until all commits have
200     succeeded. Regardless, it will be up to your application to act on the failures and work with
201     the user to resolve all failures until the nested store records are all clean.
202 
203 
204     @param {Hash} [attrs] attributes to set on new store
205     @param {Class} [newStoreClass] the class of the newly-created nested store (defaults to SC.NestedStore)
206     @returns {SC.NestedStore} new nested store chained to receiver
207   */
208   chainAutonomousStore: function(attrs, newStoreClass) {
209     var newAttrs = attrs ? SC.clone( attrs ) : {};
210     var source  = this._getDataSource();
211 
212     newAttrs.dataSource = source;
213     return this.chain( newAttrs, newStoreClass );
214   },
215 
216   /** @private
217 
218     Called by a nested store just before it is destroyed so that the parent
219     can remove the store from its list of nested stores.
220 
221     @returns {SC.Store} receiver
222   */
223   willDestroyNestedStore: function(nestedStore) {
224     if (this.nestedStores) {
225       this.nestedStores.removeObject(nestedStore);
226     }
227     return this ;
228   },
229 
230   /**
231     Used to determine if a nested store belongs directly or indirectly to the
232     receiver.
233 
234     @param {SC.Store} store store instance
235     @returns {Boolean} YES if belongs
236   */
237   hasNestedStore: function(store) {
238     while(store && (store !== this)) store = store.get('parentStore');
239     return store === this ;
240   },
241 
242   // ..........................................................
243   // SHARED DATA STRUCTURES
244   //
245 
246   /** @private
247     JSON data hashes indexed by store key.
248 
249     *IMPORTANT: Property is not observable*
250 
251     Shared by a store and its child stores until you make edits to it.
252 
253     @type Hash
254   */
255   dataHashes: null,
256 
257   /** @private
258     The current status of a data hash indexed by store key.
259 
260     *IMPORTANT: Property is not observable*
261 
262     Shared by a store and its child stores until you make edits to it.
263 
264     @type Hash
265   */
266   statuses: null,
267 
268   /** @private
269     This array contains the revisions for the attributes indexed by the
270     storeKey.
271 
272     *IMPORTANT: Property is not observable*
273 
274     Revisions are used to keep track of when an attribute hash has been
275     changed. A store shares the revisions data with its parent until it
276     starts to make changes to it.
277 
278     @type Hash
279   */
280   revisions: null,
281 
282   /**
283     Array indicates whether a data hash is possibly in use by an external
284     record for editing.  If a data hash is editable then it may be modified
285     at any time and therefore chained stores may need to clone the
286     attributes before keeping a copy of them.
287 
288     Note that this is kept as an array because it will be stored as a dense
289     array on some browsers, making it faster.
290 
291     @type Array
292   */
293   editables: null,
294 
295   /**
296     A set of storeKeys that need to be committed back to the data source. If
297     you call `commitRecords()` without passing any other parameters, the keys
298     in this set will be committed instead.
299 
300     @type SC.Set
301   */
302   changelog: null,
303 
304   /**
305     An array of `SC.Error` objects associated with individual records in the
306     store (indexed by store keys).
307 
308     Errors passed form the data source in the call to dataSourceDidError() are
309     stored here.
310 
311     @type Array
312   */
313   recordErrors: null,
314 
315   /**
316     A hash of `SC.Error` objects associated with queries (indexed by the GUID
317     of the query).
318 
319     Errors passed from the data source in the call to
320     `dataSourceDidErrorQuery()` are stored here.
321 
322     @type Hash
323   */
324   queryErrors: null,
325 
326   /**
327     A hash of child Records and there immediate parents
328   */
329   childRecords: null,
330 
331   /**
332     A hash of parent records with registered children
333   */
334   parentRecords: null,
335 
336   // ..........................................................
337   // CORE ATTRIBUTE API
338   //
339   // The methods in this layer work on data hashes in the store.  They do not
340   // perform any changes that can impact records.  Usually you will not need
341   // to use these methods.
342 
343   /**
344     Returns the current edit status of a store key.  May be one of
345     `EDITABLE` or `LOCKED`.  Used mostly for unit testing.
346 
347     @param {Number} storeKey the store key
348     @returns {Number} edit status
349   */
350   storeKeyEditState: function(storeKey) {
351     var editables = this.editables;
352     return (editables && editables[storeKey]) ? SC.Store.EDITABLE : SC.Store.LOCKED ;
353   },
354 
355   /**
356     Returns the data hash for the given `storeKey`.  This will also 'lock'
357     the hash so that further edits to the parent store will no
358     longer be reflected in this store until you reset.
359 
360     @param {Number} storeKey key to retrieve
361     @returns {Hash} data hash or null
362   */
363   readDataHash: function(storeKey) {
364     return this.dataHashes[storeKey];
365   },
366 
367   /**
368     Returns the data hash for the `storeKey`, cloned so that you can edit
369     the contents of the attributes if you like.  This will do the extra work
370     to make sure that you only clone the attributes one time.
371 
372     If you use this method to modify data hash, be sure to call
373     `dataHashDidChange()` when you make edits to record the change.
374 
375     @param {Number} storeKey the store key to retrieve
376     @returns {Hash} the attributes hash
377   */
378   readEditableDataHash: function(storeKey) {
379     // read the value - if there is no hash just return; nothing to do
380     var ret = this.dataHashes[storeKey];
381     if (!ret) return ret ; // nothing to do.
382 
383     // clone data hash if not editable
384     var editables = this.editables;
385     if (!editables) editables = this.editables = [];
386     if (!editables[storeKey]) {
387       editables[storeKey] = 1 ; // use number to store as dense array
388       ret = this.dataHashes[storeKey] = SC.clone(ret, YES);
389     }
390     return ret;
391   },
392 
393   /**
394     Reads a property from the hash - cloning it if needed so you can modify
395     it independently of any parent store.  This method is really only well
396     tested for use with toMany relationships.  Although it is public you
397     generally should not call it directly.
398 
399     @param {Number} storeKey storeKey of data hash
400     @param {String} propertyName property to read
401     @returns {Object} editable property value
402   */
403   readEditableProperty: function(storeKey, propertyName) {
404     var hash      = this.readEditableDataHash(storeKey),
405         editables = this.editables[storeKey], // get editable info...
406         ret       = hash[propertyName];
407 
408     // editables must be made into a hash so that we can keep track of which
409     // properties have already been made editable
410     if (editables === 1) editables = this.editables[storeKey] = {};
411 
412     // clone if needed
413     if (!editables[propertyName]) {
414       ret = hash[propertyName];
415       if (ret && ret.isCopyable) ret = hash[propertyName] = ret.copy(YES);
416       editables[propertyName] = YES ;
417     }
418 
419     return ret ;
420   },
421 
422   /**
423     Replaces the data hash for the `storeKey`.  This will lock the data hash
424     and mark them as cloned.  This will also call `dataHashDidChange()` for
425     you.
426 
427     Note that the hash you set here must be a different object from the
428     original data hash.  Once you make a change here, you must also call
429     `dataHashDidChange()` to register the changes.
430 
431     If the data hash does not yet exist in the store, this method will add it.
432     Pass the optional status to edit the status as well.
433 
434     @param {Number} storeKey the store key to write
435     @param {Hash} hash the new hash
436     @param {String} status the new hash status
437     @returns {SC.Store} receiver
438   */
439   writeDataHash: function(storeKey, hash, status) {
440 
441     // update dataHashes and optionally status.
442     if (hash) this.dataHashes[storeKey] = hash;
443     if (status) this.statuses[storeKey] = status ;
444 
445     // also note that this hash is now editable
446     var editables = this.editables;
447     if (!editables) editables = this.editables = [];
448     editables[storeKey] = 1 ; // use number for dense array support
449 
450     // propagate the data to the child records
451     this._updateChildRecordHashes(storeKey, hash, status);
452 
453     return this ;
454   },
455 
456   /** @private
457 
458     Called by writeDataHash to update the child record hashes starting from the new (parent) data hash.
459 
460     @returns {SC.Store} receiver
461   */
462   _updateChildRecordHashes: function(storeKey, hash, status) {
463     // Update the child record hashes in place.
464     if (!SC.none(this.parentRecords) ) {
465       // All previously materialized nested objects.
466       var materializedNestedObjects = this.parentRecords[storeKey] || {},
467         processedPaths = {},
468         childHash;
469 
470       for (var key in materializedNestedObjects) {
471         if (materializedNestedObjects.hasOwnProperty(key)) {
472 
473           // If there is a value for the nested object.
474           if (hash) {
475             var childPath = materializedNestedObjects[key],
476                 nestedIndex,
477                 nestedProperty;
478 
479             // Note: toMany nested objects have a path indicating their index, ex. 'children.0'
480             childPath = childPath.split('.');
481             nestedProperty = childPath[0];
482             nestedIndex = childPath[1];
483             /*jshint eqnull:true*/
484             if (nestedIndex != null) {
485               // The hash value for this particular object in the array of the record.
486               // ex. '{ children: [{ ... }, { ...] }'
487               childHash = hash[nestedProperty][nestedIndex];
488             } else {
489               // The hash value for this object on the record.
490               // ex. '{ child: { ... } }'
491               childHash = hash[nestedProperty];
492             }
493 
494             // Update the data hash for the materialized nested record.
495             this.writeDataHash(key, childHash, status);
496 
497             // Problem: If the materialized nested object is in an array, how do we update only that
498             // nested object when its position in the array may have changed?
499             // Ex. children: [{ n: 'A' }] => children: [{ n: 'B' }, { n: 'A' }]
500             // If { n: 'A' } was materialized, with child path 'children.0', if we updated the hash
501             // to be the latest object at index 0, that materialized record would suddenly be backed
502             // as { n: 'B' }. The only solution, short of an entire re-architect of nested objects,
503             // seems to be to force materialize all items in the array.
504             // NOTE: This is a problem because nested objects are masquerading as records. This will
505             // be fixed by fixing the concept of nested records to be nested objects.
506             // FURTHER NOTE: This also seems to be a usage/expectations problem. Is it not correct
507             // for the first materialized child object to update itself?
508             if (nestedIndex != null && processedPaths[nestedProperty] == null) {
509               // save it so that we don't process it over and over
510               processedPaths[nestedProperty] = true;
511 
512               // force fetching of all children records by invoking the children_attribute wrapper code
513               // and then interating the list in an empty loop
514               // Ugly, but there's basically no other way to do it at the moment, other than
515               // leaving this broken as it was before
516               var siblings = this.records[storeKey].get(nestedProperty);
517               for (var i = 0, len = siblings.get('length'); i < len; i++) {
518                 if (i !== nestedIndex) { // Don't materialize this same one again.
519                   siblings.objectAt(i); // Get the sibling to materialize it.
520                 }
521               }
522             }
523           } else {
524             this.writeDataHash(key, null, status);
525           }
526         }
527       }
528     }
529   },
530 
531   /**
532     Removes the data hash from the store.  This does not imply a deletion of
533     the record.  You could be simply unloading the record.  Either way,
534     removing the dataHash will be synced back to the parent store but not to
535     the server.
536 
537     Note that you can optionally pass a new status to go along with this. If
538     you do not pass a status, it will change the status to `SC.RECORD_EMPTY`
539     (assuming you just unloaded the record).  If you are deleting the record
540     you may set it to `SC.Record.DESTROYED_CLEAN`.
541 
542     Be sure to also call `dataHashDidChange()` to register this change.
543 
544     @param {Number} storeKey
545     @param {String} status optional new status
546     @returns {SC.Store} receiver
547   */
548   removeDataHash: function(storeKey, status) {
549      // don't use delete -- that will allow parent dataHash to come through
550     this.dataHashes[storeKey] = null;
551     this.statuses[storeKey] = status || SC.Record.EMPTY;
552 
553     // hash is gone and therefore no longer editable
554     var editables = this.editables;
555     if (editables) editables[storeKey] = 0 ;
556 
557     return this ;
558   },
559 
560   /**
561     Reads the current status for a storeKey.  This will also lock the data
562     hash.  If no status is found, returns `SC.RECORD_EMPTY`.
563 
564     @param {Number} storeKey the store key
565     @returns {Number} status
566   */
567   readStatus: function(storeKey) {
568     // use readDataHash to handle optimistic locking.  this could be inlined
569     // but for now this minimized copy-and-paste code.
570     this.readDataHash(storeKey);
571     return this.statuses[storeKey] || SC.Record.EMPTY;
572   },
573 
574   /**
575     Reads the current status for the storeKey without actually locking the
576     record.  Usually you won't need to use this method.  It is mostly used
577     internally.
578 
579     @param {Number} storeKey the store key
580     @returns {Number} status
581   */
582   peekStatus: function(storeKey) {
583     return this.statuses[storeKey] || SC.Record.EMPTY;
584   },
585 
586   /**
587     Writes the current status for a storeKey.  If the new status is
588     `SC.Record.ERROR`, you may also pass an optional error object.  Otherwise
589     this param is ignored.
590 
591     @param {Number} storeKey the store key
592     @param {String} newStatus the new status
593     @param {SC.Error} error optional error object
594     @returns {SC.Store} receiver
595   */
596   writeStatus: function(storeKey, newStatus) {
597     var that = this,
598         ret;
599     // use writeDataHash for now to handle optimistic lock.  maximize code
600     // reuse.
601     ret = this.writeDataHash(storeKey, null, newStatus);
602     this._propagateToChildren(storeKey, function(storeKey) {
603       that.writeStatus(storeKey, newStatus);
604     });
605     return ret;
606   },
607 
608   /**
609     Call this method whenever you modify some editable data hash to register
610     with the Store that the attribute values have actually changed.  This will
611     do the book-keeping necessary to track the change across stores including
612     managing locks.
613 
614     @param {Number|Array} storeKeys one or more store keys that changed
615     @param {Number} rev optional new revision number. normally leave null
616     @param {Boolean} statusOnly (optional) YES if only status changed
617     @param {String} key that changed (optional)
618     @returns {SC.Store} receiver
619   */
620   dataHashDidChange: function(storeKeys, rev, statusOnly, key) {
621     // update the revision for storeKey.  Use generateStoreKey() because that
622     // guarantees a universally (to this store hierarchy anyway) unique
623     // key value.
624     if (!rev) rev = SC.Store.generateStoreKey();
625     var isArray, len, idx, storeKey;
626 
627     isArray = SC.typeOf(storeKeys) === SC.T_ARRAY;
628     if (isArray) {
629       len = storeKeys.length;
630     } else {
631       len = 1;
632       storeKey = storeKeys;
633     }
634 
635     var that = this;
636     for(idx=0;idx<len;idx++) {
637       if (isArray) storeKey = storeKeys[idx];
638       this.revisions[storeKey] = rev;
639       this._notifyRecordPropertyChange(storeKey, statusOnly, key);
640 
641       this._propagateToChildren(storeKey, function(storeKey){
642         that.dataHashDidChange(storeKey, null, statusOnly, key);
643       });
644     }
645 
646     return this ;
647   },
648 
649   /** @private
650     Will push all changes to a the recordPropertyChanges property
651     and execute `flush()` once at the end of the runloop.
652   */
653   _notifyRecordPropertyChange: function(storeKey, statusOnly, key) {
654 
655     var records      = this.records,
656         nestedStores = this.get('nestedStores'),
657         K            = SC.Store,
658         rec, editState, len, idx, store, status, keys;
659 
660     // pass along to nested stores
661     len = nestedStores ? nestedStores.length : 0 ;
662     for(idx=0;idx<len;idx++) {
663       store = nestedStores[idx];
664       status = store.peekStatus(storeKey); // important: peek avoids read-lock
665       editState = store.storeKeyEditState(storeKey);
666 
667       // when store needs to propagate out changes in the parent store
668       // to nested stores
669       if (editState === K.INHERITED) {
670         store._notifyRecordPropertyChange(storeKey, statusOnly, key);
671 
672       } else if (status & SC.Record.BUSY) {
673         // make sure nested store does not have any changes before resetting
674         if(store.get('hasChanges')) K.CHAIN_CONFLICT_ERROR.throw();
675         store.reset();
676       }
677     }
678 
679     // store info in changes hash and schedule notification if needed.
680     var changes = this.recordPropertyChanges;
681     if (!changes) {
682       changes = this.recordPropertyChanges =
683         { storeKeys:      SC.CoreSet.create(),
684           records:        SC.CoreSet.create(),
685           hasDataChanges: SC.CoreSet.create(),
686           propertyForStoreKeys: {} };
687     }
688 
689     changes.storeKeys.add(storeKey);
690 
691     if (records && (rec=records[storeKey])) {
692       changes.records.push(storeKey);
693 
694       // If there are changes other than just the status we need to record
695       // that information so we do the right thing during the next flush.
696       // Note that if we're called multiple times before flush and one call
697       // has `statusOnly=true` and another has `statusOnly=false`, the flush
698       // will (correctly) operate in `statusOnly=false` mode.
699       if (!statusOnly) changes.hasDataChanges.push(storeKey);
700 
701       // If this is a key specific change, make sure that only those
702       // properties/keys are notified.  However, if a previous invocation of
703       // `_notifyRecordPropertyChange` specified that all keys have changed, we
704       // need to respect that.
705       if (key) {
706         if (!(keys = changes.propertyForStoreKeys[storeKey])) {
707           keys = changes.propertyForStoreKeys[storeKey] = SC.CoreSet.create();
708         }
709 
710         // If it's '*' instead of a set, then that means there was a previous
711         // invocation that said all keys have changed.
712         if (keys !== '*') {
713           keys.add(key);
714         }
715       }
716       else {
717         // Mark that all properties have changed.
718         changes.propertyForStoreKeys[storeKey] = '*';
719       }
720     }
721 
722     this.invokeOnce(this.flush);
723     return this;
724   },
725 
726   /**
727     Delivers any pending changes to materialized records.  Normally this
728     happens once, automatically, at the end of the RunLoop.  If you have
729     updated some records and need to update records immediately, however,
730     you may call this manually.
731 
732     @returns {SC.Store} receiver
733   */
734   flush: function() {
735     if (!this.recordPropertyChanges) return this;
736 
737     var changes              = this.recordPropertyChanges,
738         storeKeys            = changes.storeKeys,
739         hasDataChanges       = changes.hasDataChanges,
740         records              = changes.records,
741         propertyForStoreKeys = changes.propertyForStoreKeys,
742         recordTypes = SC.CoreSet.create(),
743         rec, recordType, statusOnly, keys;
744 
745     storeKeys.forEach(function(storeKey) {
746       if (records.contains(storeKey)) {
747         statusOnly = hasDataChanges.contains(storeKey) ? NO : YES;
748         rec = this.records[storeKey];
749         keys = propertyForStoreKeys ? propertyForStoreKeys[storeKey] : null;
750 
751         // Are we invalidating all keys?  If so, don't pass any to
752         // storeDidChangeProperties.
753         if (keys === '*') keys = null;
754 
755         // remove it so we don't trigger this twice
756         records.remove(storeKey);
757 
758         if (rec) rec.storeDidChangeProperties(statusOnly, keys);
759       }
760 
761       recordType = SC.Store.recordTypeFor(storeKey);
762       recordTypes.add(recordType);
763 
764     }, this);
765 
766     if (storeKeys.get('length') > 0) this._notifyRecordArrays(storeKeys, recordTypes);
767 
768     storeKeys.clear();
769     hasDataChanges.clear();
770     records.clear();
771     // Provide full reference to overwrite
772     this.recordPropertyChanges.propertyForStoreKeys = {};
773 
774     return this;
775   },
776 
777   /**
778     Resets the store content.  This will clear all internal data for all
779     records, resetting them to an EMPTY state.  You generally do not want
780     to call this method yourself, though you may override it.
781 
782     @returns {SC.Store} receiver
783   */
784   reset: function() {
785 
786     // create a new empty data store
787     this.dataHashes = {} ;
788     this.revisions  = {} ;
789     this.statuses   = {} ;
790     this.records = {};
791     this.childRecords = {};
792     this.parentRecords = {};
793 
794     // also reset temporary objects and errors
795     this.chainedChanges = this.locks = this.editables = null;
796     this.changelog = null ;
797     this.recordErrors = null;
798     this.queryErrors = null;
799 
800     var dataSource = this.get('dataSource');
801     if (dataSource && dataSource.reset) { dataSource.reset(); }
802 
803     var records = this.records, storeKey;
804     if (records) {
805       for(storeKey in records) {
806         if (!records.hasOwnProperty(storeKey)) continue ;
807         this._notifyRecordPropertyChange(parseInt(storeKey, 10), NO);
808       }
809     }
810 
811     // Also reset all pre-created recordArrays.
812     var ra, raList = this.get('recordArrays');
813     if (raList) {
814       while ((ra = raList.pop())) {
815         ra.destroy();
816       }
817       raList.clear();
818       this.set('recordArrays', null);
819     }
820 
821     this.set('hasChanges', NO);
822   },
823 
824   /** @private
825     Called by a nested store on a parent store to commit any changes from the
826     store.  This will copy any changed dataHashes as well as any persistent
827     change logs.
828 
829     If the parentStore detects a conflict with the optimistic locking, it will
830     raise an exception before it makes any changes.  If you pass the
831     force flag then this detection phase will be skipped and the changes will
832     be applied even if another resource has modified the store in the mean
833     time.
834 
835     @param {SC.Store} nestedStore the child store
836     @param {SC.Set} changes the set of changed store keys
837     @param {Boolean} force
838     @returns {SC.Store} receiver
839   */
840   commitChangesFromNestedStore: function (nestedStore, changes, force) {
841     // first, check for optimistic locking problems
842     if (!force && nestedStore.get('conflictedStoreKeys')) {
843       SC.Store.CHAIN_CONFLICT_ERROR.throw();
844     }
845 
846     // OK, no locking issues.  So let's just copy them changes.
847     // get local reference to values.
848     var len = changes.length, i, storeKey, myDataHashes, myStatuses,
849       myEditables, myRevisions, myParentRecords, myChildRecords,
850       chDataHashes, chStatuses, chRevisions, chParentRecords, chChildRecords;
851 
852     myRevisions     = this.revisions ;
853     myDataHashes    = this.dataHashes;
854     myStatuses      = this.statuses;
855     myEditables     = this.editables ;
856     myParentRecords = this.parentRecords ? this.parentRecords : this.parentRecords ={} ;
857     myChildRecords  = this.childRecords ? this.childRecords : this.childRecords = {} ;
858 
859     // setup some arrays if needed
860     if (!myEditables) myEditables = this.editables = [] ;
861     chDataHashes    = nestedStore.dataHashes;
862     chRevisions     = nestedStore.revisions ;
863     chStatuses      = nestedStore.statuses;
864     chParentRecords = nestedStore.parentRecords || {};
865     chChildRecords  = nestedStore.childRecords || {};
866 
867     for(i=0;i<len;i++) {
868       storeKey = changes[i];
869 
870       // now copy changes
871       myDataHashes[storeKey]    = chDataHashes[storeKey];
872       myStatuses[storeKey]      = chStatuses[storeKey];
873       myRevisions[storeKey]     = chRevisions[storeKey];
874       myParentRecords[storeKey] = chParentRecords[storeKey];
875       myChildRecords[storeKey]  = chChildRecords[storeKey];
876 
877       myEditables[storeKey] = 0 ; // always make dataHash no longer editable
878 
879       this._notifyRecordPropertyChange(storeKey, NO);
880     }
881 
882     // add any records to the changelog for commit handling
883     var myChangelog = this.changelog, chChangelog = nestedStore.changelog;
884     if (chChangelog) {
885       if (!myChangelog) myChangelog = this.changelog = SC.CoreSet.create();
886       myChangelog.addEach(chChangelog);
887     }
888     this.changelog = myChangelog;
889 
890     // immediately flush changes to notify records - nested stores will flush
891     // later on.
892     if (!this.get('parentStore')) this.flush();
893 
894     return this ;
895   },
896 
897   // ..........................................................
898   // HIGH-LEVEL RECORD API
899   //
900 
901   /**
902     Finds a single record instance with the specified `recordType` and id or
903     an  array of records matching some query conditions.
904 
905     Finding a Single Record
906     ---
907 
908     If you pass a single `recordType` and id, this method will return an
909     actual record instance.  If the record has not been loaded into the store
910     yet, this method will ask the data source to retrieve it.  If no data
911     source indicates that it can retrieve the record, then this method will
912     return `null`.
913 
914     Note that if the record needs to be retrieved from the server, then the
915     record instance returned from this method will not have any data yet.
916     Instead it will have a status of `SC.Record.READY_LOADING`.  You can
917     monitor the status property to be notified when the record data is
918     available for you to use it.
919 
920     Find a Collection of Records
921     ---
922 
923     If you pass only a record type or a query object, you can instead find
924     all records matching a specified set of conditions.  When you call
925     `find()` in this way, it will create a query if needed and pass it to the
926     data source to fetch the results.
927 
928     If this is the first time you have fetched the query, then the store will
929     automatically ask the data source to fetch any records related to it as
930     well.  Otherwise you can refresh the query results at anytime by calling
931     `refresh()` on the returned `RecordArray`.
932 
933     You can detect whether a RecordArray is fetching from the server by
934     checking its status.
935 
936     Examples
937     ---
938 
939     Finding a single record:
940 
941         MyApp.store.find(MyApp.Contact, "23"); // returns MyApp.Contact
942 
943     Finding all records of a particular type:
944 
945         MyApp.store.find(MyApp.Contact); // returns SC.RecordArray of contacts
946 
947 
948     Finding all contacts with first name John:
949 
950         var query = SC.Query.local(MyApp.Contact, "firstName = %@", "John");
951         MyApp.store.find(query); // returns SC.RecordArray of contacts
952 
953     Finding all contacts using a remote query:
954 
955         var query = SC.Query.remote(MyApp.Contact);
956         MyApp.store.find(query); // returns SC.RecordArray filled by server
957 
958     @param {SC.Record|String} recordType the expected record type
959     @param {String} id the id to load
960     @returns {SC.Record} record instance or null
961   */
962   find: function(recordType, id) {
963 
964     // if recordType is passed as string, find object
965     if (SC.typeOf(recordType)===SC.T_STRING) {
966       recordType = SC.objectForPropertyPath(recordType);
967     }
968 
969     // handle passing a query...
970     if ((arguments.length === 1) && !(recordType && recordType.get && recordType.get('isRecord'))) {
971       if (!recordType) throw new Error("SC.Store#find() must pass recordType or query");
972       if (!recordType.isQuery) {
973         recordType = SC.Query.local(recordType);
974       }
975       return this._findQuery(recordType, YES, YES);
976 
977     // handle finding a single record
978     } else {
979       return this._findRecord(recordType, id);
980     }
981   },
982 
983   /** @private */
984   _findQuery: function(query, createIfNeeded, refreshIfNew) {
985 
986     // lookup the local RecordArray for this query.
987     var cache = this._scst_recordArraysByQuery,
988         key   = SC.guidFor(query),
989         ret, ra ;
990     if (!cache) cache = this._scst_recordArraysByQuery = {};
991     ret = cache[key];
992 
993     // if a RecordArray was not found, then create one and also add it to the
994     // list of record arrays to update.
995     if (!ret && createIfNeeded) {
996       cache[key] = ret = SC.RecordArray.create({ store: this, query: query });
997 
998       ra = this.get('recordArrays');
999       if (!ra) this.set('recordArrays', ra = SC.Set.create());
1000       ra.add(ret);
1001 
1002       if (refreshIfNew) this.refreshQuery(query);
1003     }
1004 
1005     this.flush();
1006     return ret ;
1007   },
1008 
1009   /** @private */
1010   _findRecord: function(recordType, id) {
1011 
1012     var storeKey ;
1013 
1014     // if a record instance is passed, simply use the storeKey.  This allows
1015     // you to pass a record from a chained store to get the same record in the
1016     // current store.
1017     if (recordType && recordType.get && recordType.get('isRecord')) {
1018       storeKey = recordType.get('storeKey');
1019 
1020     // otherwise, lookup the storeKey for the passed id.  look in subclasses
1021     // as well.
1022     } else storeKey = id ? recordType.storeKeyFor(id) : null;
1023 
1024     if (storeKey && (this.peekStatus(storeKey) === SC.Record.EMPTY)) {
1025       storeKey = this.retrieveRecord(recordType, id);
1026     }
1027 
1028     // now we have the storeKey, materialize the record and return it.
1029     return storeKey ? this.materializeRecord(storeKey) : null ;
1030   },
1031 
1032   // ..........................................................
1033   // RECORD ARRAY OPERATIONS
1034   //
1035 
1036   /**
1037     Called by the record array just before it is destroyed.  This will
1038     de-register it from receiving future notifications.
1039 
1040     You should never call this method yourself.  Instead call `destroy()` on
1041     the `RecordArray` directly.
1042 
1043     @param {SC.RecordArray} recordArray the record array
1044     @returns {SC.Store} receiver
1045   */
1046   recordArrayWillDestroy: function(recordArray) {
1047     var cache = this._scst_recordArraysByQuery,
1048         set   = this.get('recordArrays');
1049 
1050     if (cache) delete cache[SC.guidFor(recordArray.get('query'))];
1051     if (set) set.remove(recordArray);
1052     return this ;
1053   },
1054 
1055   /**
1056     Called by the record array whenever it needs the data source to refresh
1057     its contents.  Nested stores will actually just pass this along to the
1058     parent store.  The parent store will call `fetch()` on the data source.
1059 
1060     You should never call this method yourself.  Instead call `refresh()` on
1061     the `RecordArray` directly.
1062 
1063     @param {SC.Query} query the record array query to refresh
1064     @returns {SC.Store} receiver
1065   */
1066   refreshQuery: function(query) {
1067     if (!query) throw new Error("refreshQuery() requires a query");
1068 
1069     var cache    = this._scst_recordArraysByQuery,
1070         recArray = cache ? cache[SC.guidFor(query)] : null,
1071         source   = this._getDataSource();
1072 
1073     if (source && source.fetch) {
1074       if (recArray) recArray.storeWillFetchQuery(query);
1075       source.fetch.call(source, this, query);
1076     }
1077 
1078     return this ;
1079   },
1080 
1081   /** @private
1082     Will ask all record arrays that have been returned from `find`
1083     with an `SC.Query` to check their arrays with the new `storeKey`s
1084 
1085     @param {SC.IndexSet} storeKeys set of storeKeys that changed
1086     @param {SC.Set} recordTypes
1087     @returns {SC.Store} receiver
1088   */
1089   _notifyRecordArrays: function(storeKeys, recordTypes) {
1090     var recordArrays = this.get('recordArrays');
1091     if (!recordArrays) return this;
1092 
1093     recordArrays.forEach(function(recArray) {
1094       if (recArray) recArray.storeDidChangeStoreKeys(storeKeys, recordTypes);
1095     }, this);
1096 
1097     return this ;
1098   },
1099 
1100 
1101   // ..........................................................
1102   // LOW-LEVEL HELPERS
1103   //
1104 
1105   /**
1106     Array of all records currently in the store with the specified
1107     type.  This method only reflects the actual records loaded into memory and
1108     therefore is not usually needed at runtime.  However you will often use
1109     this method for testing.
1110 
1111     @param {SC.Record} recordType the record type
1112     @returns {SC.Array} array instance - usually SC.RecordArray
1113   */
1114   recordsFor: function(recordType) {
1115     var storeKeys     = [],
1116         storeKeysById = recordType.storeKeysById(),
1117         id, storeKey, ret;
1118 
1119     // collect all non-empty store keys
1120     for(id in storeKeysById) {
1121       storeKey = storeKeysById[id]; // get the storeKey
1122       if (this.readStatus(storeKey) !== SC.RECORD_EMPTY) {
1123         storeKeys.push(storeKey);
1124       }
1125     }
1126 
1127     if (storeKeys.length>0) {
1128       ret = SC.RecordArray.create({ store: this, storeKeys: storeKeys });
1129     } else ret = storeKeys; // empty array
1130     return ret ;
1131   },
1132 
1133   /** @private */
1134   _CACHED_REC_ATTRS: {},
1135 
1136   /** @private */
1137   _CACHED_REC_INIT: function() {},
1138 
1139   /**
1140     Given a `storeKey`, return a materialized record.  You will not usually
1141     call this method yourself.  Instead it will used by other methods when
1142     you find records by id or perform other searches.
1143 
1144     If a `recordType` has been mapped to the storeKey, then a record instance
1145     will be returned even if the data hash has not been requested yet.
1146 
1147     Each Store instance returns unique record instances for each storeKey.
1148 
1149     @param {Number} storeKey The storeKey for the dataHash.
1150     @returns {SC.Record} Returns a record instance.
1151   */
1152   materializeRecord: function(storeKey) {
1153     var records = this.records,
1154       ret, recordType, attrs;
1155 
1156     // look up in cached records
1157     if (!records) records = this.records = {}; // load cached records
1158     ret = records[storeKey];
1159     if (ret) return ret;
1160 
1161     // not found -- OK, create one then.
1162     recordType = SC.Store.recordTypeFor(storeKey);
1163     if (!recordType) return null; // not recordType registered, nothing to do
1164 
1165     // Populate the attributes.
1166     attrs = this._CACHED_REC_ATTRS ;
1167     attrs.storeKey = storeKey ;
1168     attrs.store    = this ;
1169 
1170     // We do a little gymnastics here to prevent record initialization before we've
1171     // received and cached a copy of the object. This is because if initialization
1172     // triggers downstream effects which call materializeRecord for the same record,
1173     // we won't have a copy of it cached yet, causing another copy to be created
1174     // and resulting in a stack overflow at best and a really hard-to-diagnose bug
1175     // involving two instances of the same record floating around at worst.
1176 
1177     // Override _object_init to prevent premature initialization.
1178     var _object_init = recordType.prototype._object_init;
1179     recordType.prototype._object_init = this._CACHED_REC_INIT;
1180     // Create the record (but don't init it).
1181     ret = records[storeKey] = recordType.create();
1182     // Repopulate the _object_init method and run initialization.
1183     recordType.prototype._object_init = ret._object_init = _object_init;
1184     ret._object_init([attrs]);
1185 
1186     return ret ;
1187   },
1188 
1189   // ..........................................................
1190   // CORE RECORDS API
1191   //
1192   // The methods in this section can be used to manipulate records without
1193   // actually creating record instances.
1194 
1195   /**
1196     Creates a new record instance with the passed `recordType` and `dataHash`.
1197     You can also optionally specify an id or else it will be pulled from the
1198     data hash.
1199 
1200     Example:
1201 
1202       MyApp.Record = SC.Record.extend({
1203         attrA: SC.Record.attr(String, { defaultValue: 'def' }),
1204         isAttrB: SC.Record.attr(Boolean, { key: 'attr_b' }),
1205         primaryKey: 'pKey'
1206       });
1207 
1208       // If you don't provide a value and have designated a defaultValue, the
1209       // defaultValue will be used.
1210       MyApp.store.createRecord(MyApp.Record).get('attributes');
1211       > { attrA: 'def' }
1212 
1213       // If you use a key on an attribute, you can specify the key name or the
1214       // attribute name when creating the record, but if you specify both, only
1215       // the key name will be used.
1216       MyApp.store.createRecord(MyApp.Record, { isAttrB: YES }).get('attributes');
1217       > { attr_b: YES }
1218       MyApp.store.createRecord(MyApp.Record, { attr_b: YES }).get('attributes');
1219       > { attr_b: YES }
1220       MyApp.store.createRecord(MyApp.Record, { isAttrB: NO, attr_b: YES }).get('attributes');
1221       > { attr_b: YES }
1222 
1223     Note that the record will not yet be saved back to the server.  To save
1224     a record to the server, call `commitChanges()` on the store.
1225 
1226     @param {SC.Record} recordType the record class to use on creation
1227     @param {Hash} dataHash the JSON attributes to assign to the hash.
1228     @param {String} id (optional) id to assign to record
1229 
1230     @returns {SC.Record} Returns the created record
1231   */
1232   createRecord: function(recordType, dataHash, id) {
1233     var primaryKey, prototype, storeKey, status, K = SC.Record, changelog, defaultVal, ret;
1234 
1235     //initialize dataHash if necessary
1236     dataHash = (dataHash ? dataHash : {});
1237 
1238     // First, try to get an id.  If no id is passed, look it up in the
1239     // dataHash.
1240     if (!id && (primaryKey = recordType.prototype.primaryKey)) {
1241       id = dataHash[primaryKey];
1242       // if still no id, check if there is a defaultValue function for
1243       // the primaryKey attribute and assign that
1244       defaultVal = recordType.prototype[primaryKey] ? recordType.prototype[primaryKey].defaultValue : null;
1245       if(!id && SC.typeOf(defaultVal)===SC.T_FUNCTION) {
1246         id = dataHash[primaryKey] = defaultVal();
1247       }
1248     }
1249 
1250     // Next get the storeKey - base on id if available
1251     storeKey = id ? recordType.storeKeyFor(id) : SC.Store.generateStoreKey();
1252 
1253     // now, check the state and do the right thing.
1254     status = this.readStatus(storeKey);
1255 
1256     // check state
1257     // any busy or ready state or destroyed dirty state is not allowed
1258     if ((status & K.BUSY)  ||
1259         (status & K.READY) ||
1260         (status === K.DESTROYED_DIRTY)) {
1261       (id ? K.RECORD_EXISTS_ERROR : K.BAD_STATE_ERROR).throw();
1262 
1263     // allow error or destroyed state only with id
1264     } else if (!id && (status===SC.DESTROYED_CLEAN || status===SC.ERROR)) {
1265       K.BAD_STATE_ERROR.throw();
1266     }
1267 
1268     // Store the dataHash and setup initial status.
1269     this.writeDataHash(storeKey, dataHash, K.READY_NEW);
1270 
1271     // Register the recordType with the store.
1272     SC.Store.replaceRecordTypeFor(storeKey, recordType);
1273     this.dataHashDidChange(storeKey);
1274 
1275     // If the attribute wasn't provided in the dataHash, attempt to insert a
1276     // default value.  We have to do this after materializing the record,
1277     // because the defaultValue property may be a function that expects
1278     // the record as an argument.
1279     ret = this.materializeRecord(storeKey);
1280     prototype = recordType.prototype;
1281     for (var prop in prototype) {
1282       var propPrototype = prototype[ prop ];
1283       if (propPrototype && propPrototype.isRecordAttribute) {
1284         // Use the record attribute key if it is defined.
1285         var attrKey = propPrototype.key || prop;
1286 
1287         if (!dataHash.hasOwnProperty(attrKey)) {
1288           if (dataHash.hasOwnProperty(prop)) {
1289             // If the attribute key doesn't exist but the name does, fix it up.
1290             // (i.e. the developer has a record attribute `endDate` with a key
1291             // `end_date` on a record and when they created the record they
1292             // provided `endDate` not `end_date`)
1293             dataHash[ attrKey ] = dataHash[ prop ];
1294             delete dataHash[ prop ];
1295           } else {
1296             // If the attribute doesn't exist in the hash at all, check for a
1297             // default value to use instead.
1298             defaultVal = propPrototype.defaultValue;
1299             if (defaultVal) {
1300               if (SC.typeOf(defaultVal)===SC.T_FUNCTION) dataHash[ attrKey ] = SC.copy(defaultVal(ret, attrKey), YES);
1301               else dataHash[ attrKey ] = SC.copy(defaultVal, YES);
1302             }
1303           }
1304         } else if (attrKey !== prop && dataHash.hasOwnProperty(prop)) {
1305           // If both attrKey and prop are provided, use attrKey only.
1306           delete dataHash[ prop ];
1307         }
1308       }
1309     }
1310 
1311     // Record is now in a committable state -- add storeKey to changelog
1312     changelog = this.changelog;
1313     if (!changelog) changelog = SC.Set.create();
1314     changelog.add(storeKey);
1315     this.changelog = changelog;
1316 
1317     // if commit records is enabled
1318     if(this.get('commitRecordsAutomatically')){
1319       this.invokeLast(this.commitRecords);
1320     }
1321 
1322     // Propagate the status to any aggregate records before returning.
1323     if (ret) ret.propagateToAggregates();
1324     return ret;
1325   },
1326 
1327   /**
1328     Creates an array of new records.  You must pass an array of `dataHash`es
1329     plus a `recordType` and, optionally, an array of ids.  This will create an
1330     array of record instances with the same record type.
1331 
1332     If you need to instead create a bunch of records with different data types
1333     you can instead pass an array of `recordType`s, one for each data hash.
1334 
1335     @param {SC.Record|Array} recordTypes class or array of classes
1336     @param {Array} dataHashes array of data hashes
1337     @param {Array} ids (optional) ids to assign to records
1338     @returns {Array} array of materialized record instances.
1339   */
1340   createRecords: function(recordTypes, dataHashes, ids) {
1341     var ret = [], recordType, id, isArray, len = dataHashes.length, idx ;
1342     isArray = SC.typeOf(recordTypes) === SC.T_ARRAY;
1343     if (!isArray) recordType = recordTypes;
1344     for(idx=0;idx<len;idx++) {
1345       if (isArray) recordType = recordTypes[idx] || SC.Record;
1346       id = ids ? ids[idx] : undefined ;
1347       ret.push(this.createRecord(recordType, dataHashes[idx], id));
1348     }
1349     return ret ;
1350   },
1351 
1352 
1353   /**
1354     Unloads a record, removing the data hash from the store.  If you try to
1355     unload a record that is already destroyed then this method will have no effect.
1356     If you unload a record that does not exist or an error then an exception
1357     will be raised.
1358 
1359     @param {SC.Record} recordType the recordType
1360     @param {String} id the record id
1361     @param {Number} storeKey (optional) if passed, ignores recordType and id
1362     @returns {SC.Store} receiver
1363   */
1364   unloadRecord: function(recordType, id, storeKey, newStatus) {
1365     if (storeKey === undefined) storeKey = recordType.storeKeyFor(id);
1366     var status = this.readStatus(storeKey), K = SC.Record;
1367     newStatus = newStatus || K.EMPTY;
1368     // handle status - ignore if destroying or destroyed
1369     if ((status === K.BUSY_DESTROYING) || (status & K.DESTROYED)) {
1370       return this; // nothing to do
1371 
1372     // error out if empty
1373     } else if (status & K.BUSY) {
1374       K.BUSY_ERROR.throw();
1375 
1376     // otherwise, destroy in dirty state
1377     } else status = newStatus ;
1378 
1379     // remove the data hash, set the new status and remove the cached record.
1380     this.removeDataHash(storeKey, status);
1381     this.dataHashDidChange(storeKey);
1382     delete this.records[storeKey];
1383 
1384     // If this record is a parent record, unregister all of its child records.
1385     var that = this;
1386     this._propagateToChildren(storeKey, function (storeKey) {
1387       that.unregisterChildFromParent(storeKey);
1388     });
1389 
1390     // If this record is a parent record, its child records have been cleared so also clear the
1391     // cached reference as well.
1392     if (this.parentRecords) {
1393       delete this.parentRecords[storeKey];
1394     }
1395 
1396     return this ;
1397   },
1398 
1399   /**
1400     Unloads a group of records.  If you have a set of record ids, unloading
1401     them this way can be faster than retrieving each record and unloading
1402     it individually.
1403 
1404     You can pass either a single `recordType` or an array of `recordType`s. If
1405     you pass a single `recordType`, then the record type will be used for each
1406     record.  If you pass an array, then each id must have a matching record
1407     type in the array.
1408 
1409     You can optionally pass an array of `storeKey`s instead of the `recordType`
1410     and ids.  In this case the first two parameters will be ignored.  This
1411     is usually only used by low-level internal methods.  You will not usually
1412     unload records this way.
1413 
1414     @param {SC.Record|Array} recordTypes class or array of classes
1415     @param {Array} [ids] ids to unload
1416     @param {Array} [storeKeys] store keys to unload
1417     @returns {SC.Store} receiver
1418   */
1419   unloadRecords: function(recordTypes, ids, storeKeys, newStatus) {
1420     var len, isArray, idx, id, recordType, storeKey;
1421 
1422     if (storeKeys === undefined) {
1423       isArray = SC.typeOf(recordTypes) === SC.T_ARRAY;
1424       if (!isArray) recordType = recordTypes;
1425       if (ids === undefined) {
1426         len = isArray ? recordTypes.length : 1;
1427         for (idx = 0; idx < len; idx++) {
1428           if (isArray) recordType = recordTypes[idx];
1429           storeKeys = this.storeKeysFor(recordType);
1430           this.unloadRecords(undefined, undefined, storeKeys, newStatus);
1431         }
1432       } else {
1433         len = ids.length;
1434         for (idx = 0; idx < len; idx++) {
1435           if (isArray) recordType = recordTypes[idx] || SC.Record;
1436           id = ids ? ids[idx] : undefined;
1437           this.unloadRecord(recordType, id, undefined, newStatus);
1438         }
1439       }
1440     } else {
1441       len = storeKeys.length;
1442       for (idx = 0; idx < len; idx++) {
1443         storeKey = storeKeys ? storeKeys[idx] : undefined;
1444         this.unloadRecord(undefined, undefined, storeKey, newStatus);
1445       }
1446     }
1447 
1448     return this;
1449   },
1450 
1451   /**
1452     Destroys a record, removing the data hash from the store and adding the
1453     record to the destroyed changelog.  If you try to destroy a record that is
1454     already destroyed then this method will have no effect.  If you destroy a
1455     record that does not exist or an error then an exception will be raised.
1456 
1457     @param {SC.Record} recordType the recordType
1458     @param {String} id the record id
1459     @param {Number} storeKey (optional) if passed, ignores recordType and id
1460     @returns {SC.Store} receiver
1461   */
1462   destroyRecord: function(recordType, id, storeKey) {
1463     if (storeKey === undefined) storeKey = recordType.storeKeyFor(id);
1464     var status = this.readStatus(storeKey), changelog, K = SC.Record;
1465 
1466     // handle status - ignore if destroying or destroyed
1467     if ((status === K.BUSY_DESTROYING) || (status & K.DESTROYED)) {
1468       return this; // nothing to do
1469 
1470     // error out if empty
1471     } else if (status === K.EMPTY) {
1472       K.NOT_FOUND_ERROR.throw();
1473 
1474     // error out if busy
1475     } else if (status & K.BUSY) {
1476       K.BUSY_ERROR.throw();
1477 
1478     // if new status, destroy in clean state
1479     } else if (status === K.READY_NEW) {
1480       status = K.DESTROYED_CLEAN ;
1481       this.removeDataHash(storeKey, status) ;
1482 
1483     // otherwise, destroy in dirty state
1484     } else status = K.DESTROYED_DIRTY ;
1485 
1486     // remove the data hash, set new status
1487     this.writeStatus(storeKey, status);
1488     this.dataHashDidChange(storeKey);
1489 
1490     // add/remove change log
1491     changelog = this.changelog;
1492     if (!changelog) changelog = this.changelog = SC.Set.create();
1493 
1494     if (status & K.DIRTY) { changelog.add(storeKey); }
1495     else { changelog.remove(storeKey); }
1496     this.changelog=changelog;
1497 
1498     // if commit records is enabled
1499     if(this.get('commitRecordsAutomatically')){
1500       this.invokeLast(this.commitRecords);
1501     }
1502 
1503     var that = this;
1504     this._propagateToChildren(storeKey, function(storeKey){
1505       that.destroyRecord(null, null, storeKey);
1506     });
1507 
1508     return this ;
1509   },
1510 
1511   /**
1512     Destroys a group of records.  If you have a set of record ids, destroying
1513     them this way can be faster than retrieving each record and destroying
1514     it individually.
1515 
1516     You can pass either a single `recordType` or an array of `recordType`s. If
1517     you pass a single `recordType`, then the record type will be used for each
1518     record.  If you pass an array, then each id must have a matching record
1519     type in the array.
1520 
1521     You can optionally pass an array of `storeKey`s instead of the `recordType`
1522     and ids.  In this case the first two parameters will be ignored.  This
1523     is usually only used by low-level internal methods.  You will not usually
1524     destroy records this way.
1525 
1526     @param {SC.Record|Array} recordTypes class or array of classes
1527     @param {Array} ids ids to destroy
1528     @param {Array} storeKeys (optional) store keys to destroy
1529     @returns {SC.Store} receiver
1530   */
1531   destroyRecords: function(recordTypes, ids, storeKeys) {
1532     var len, isArray, idx, id, recordType, storeKey;
1533     if(storeKeys===undefined){
1534       len = ids.length;
1535       isArray = SC.typeOf(recordTypes) === SC.T_ARRAY;
1536       if (!isArray) recordType = recordTypes;
1537       for(idx=0;idx<len;idx++) {
1538         if (isArray) recordType = recordTypes[idx] || SC.Record;
1539         id = ids ? ids[idx] : undefined ;
1540         this.destroyRecord(recordType, id, undefined);
1541       }
1542     }else{
1543       len = storeKeys.length;
1544       for(idx=0;idx<len;idx++) {
1545         storeKey = storeKeys ? storeKeys[idx] : undefined ;
1546         this.destroyRecord(undefined, undefined, storeKey);
1547       }
1548     }
1549     return this ;
1550   },
1551 
1552   /**
1553     register a Child Record to the parent
1554   */
1555   registerChildToParent: function(parentStoreKey, childStoreKey, path){
1556     var parentRecords, childRecords, oldPk, oldChildren, pkRef;
1557 
1558     // Check the child to see if it has a parent
1559     childRecords = this.childRecords || {};
1560     parentRecords = this.parentRecords || {};
1561 
1562     // first rid of the old parent
1563     oldPk = childRecords[childStoreKey];
1564     if (oldPk){
1565       oldChildren = parentRecords[oldPk];
1566       delete oldChildren[childStoreKey];
1567       // this.recordDidChange(null, null, oldPk, key);
1568     }
1569     pkRef = parentRecords[parentStoreKey] || {};
1570     pkRef[childStoreKey] = path || YES;
1571     parentRecords[parentStoreKey] = pkRef;
1572     childRecords[childStoreKey] = parentStoreKey;
1573 
1574     // sync the status of the child
1575     this.writeStatus(childStoreKey, this.statuses[parentStoreKey]);
1576     this.childRecords = childRecords;
1577     this.parentRecords = parentRecords;
1578   },
1579 
1580   /**
1581     Unregister the Child Record from its Parent.  This will cause the Child
1582     Record to be removed from the store.
1583 
1584     @param {Number} childStoreKey storeKey to unregister
1585   */
1586   unregisterChildFromParent: function(childStoreKey) {
1587     var childRecords, oldPk, storeKeys,
1588         recordType = this.recordTypeFor(childStoreKey),
1589         id = this.idFor(childStoreKey),
1590         that = this;
1591 
1592     // Check the child to see if it has a parent
1593     childRecords = this.childRecords;
1594 
1595     // Remove the child.
1596     // 1. from the cache of data hashes
1597     // 2. from the cache of record objects
1598     // 3. from the cache of child record store keys
1599     this.removeDataHash(childStoreKey);
1600     if (this.records) {
1601       delete this.records[childStoreKey];
1602     }
1603 
1604     if (childRecords) {
1605       // Remove the parent's connection to the child.  This doesn't remove the
1606       // parent store key from the cache of parent store keys if the parent
1607       // no longer has any other registered children, because the amount of effort
1608       // to determine that would not be worth the miniscule memory savings.
1609       oldPk = childRecords[childStoreKey];
1610       if (oldPk) {
1611         delete this.parentRecords[oldPk][childStoreKey];
1612       }
1613 
1614       delete childRecords[childStoreKey];
1615     }
1616 
1617     // 4. from the cache of ids
1618     // 5. from the cache of store keys
1619     delete SC.Store.idsByStoreKey[childStoreKey];
1620     storeKeys = recordType.storeKeysById();
1621     delete storeKeys[id];
1622 
1623     this._propagateToChildren(childStoreKey, function(storeKey) {
1624       that.unregisterChildFromParent(storeKey);
1625     });
1626   },
1627 
1628   /**
1629     materialize the parent when passing in a store key for the child
1630   */
1631   materializeParentRecord: function(childStoreKey){
1632     var pk, crs;
1633     if (SC.none(childStoreKey)) return null;
1634     crs = this.childRecords;
1635     pk = crs ? this.childRecords[childStoreKey] : null ;
1636     if (SC.none(pk)) return null;
1637 
1638     return this.materializeRecord(pk);
1639   },
1640 
1641   /**
1642     function for retrieving a parent record key
1643 
1644     @param {Number} storeKey The store key of the parent
1645   */
1646   parentStoreKeyExists: function(storeKey){
1647     if (SC.none(storeKey)) return ;
1648     var crs = this.childRecords || {};
1649     return crs[storeKey];
1650   },
1651 
1652   /**
1653     function that propagates a function call to all children
1654   */
1655   _propagateToChildren: function(storeKey, func){
1656     // Handle all the child Records
1657     if ( SC.none(this.parentRecords) ) return;
1658     var children = this.parentRecords[storeKey] || {};
1659     if (SC.none(func)) return;
1660     for (var key in children) {
1661       // for .. in makes the key a String, but be sure to pass a Number to the
1662       // function.
1663       if (children.hasOwnProperty(key)) func(parseInt(key, 10));
1664     }
1665   },
1666 
1667   /**
1668     Notes that the data for the given record id has changed.  The record will
1669     be committed to the server the next time you commit the root store.  Only
1670     call this method on a record in a READY state of some type.
1671 
1672     @param {SC.Record} recordType the recordType
1673     @param {String} id the record id
1674     @param {Number} storeKey (optional) if passed, ignores recordType and id
1675     @param {String} key that changed (optional)
1676     @param {Boolean} if the change is to statusOnly (optional)
1677     @returns {SC.Store} receiver
1678   */
1679   recordDidChange: function(recordType, id, storeKey, key, statusOnly) {
1680     if (storeKey === undefined) storeKey = recordType.storeKeyFor(id);
1681     var status = this.readStatus(storeKey), changelog, K = SC.Record;
1682 
1683     // BUSY_LOADING, BUSY_CREATING, BUSY_COMMITTING, BUSY_REFRESH_CLEAN
1684     // BUSY_REFRESH_DIRTY, BUSY_DESTROYING
1685     if (status & K.BUSY) {
1686       K.BUSY_ERROR.throw();
1687 
1688     // if record is not in ready state, then it is not found.
1689     // ERROR, EMPTY, DESTROYED_CLEAN, DESTROYED_DIRTY
1690     } else if (!(status & K.READY)) {
1691       K.NOT_FOUND_ERROR.throw();
1692 
1693     // otherwise, make new status READY_DIRTY unless new.
1694     // K.READY_CLEAN, K.READY_DIRTY, ignore K.READY_NEW
1695     } else {
1696       if (status !== K.READY_NEW) this.writeStatus(storeKey, K.READY_DIRTY);
1697     }
1698 
1699     // record data hash change
1700     this.dataHashDidChange(storeKey, null, statusOnly, key);
1701 
1702     // record in changelog
1703     changelog = this.changelog ;
1704     if (!changelog) changelog = this.changelog = SC.Set.create() ;
1705     changelog.add(storeKey);
1706     this.changelog = changelog;
1707 
1708     // if commit records is enabled
1709     if(this.get('commitRecordsAutomatically')){
1710       this.invokeLast(this.commitRecords);
1711     }
1712 
1713     return this ;
1714   },
1715 
1716   /**
1717     Mark a group of records as dirty.  The records will be committed to the
1718     server the next time you commit changes on the root store.  If you have a
1719     set of record ids, marking them dirty this way can be faster than
1720     retrieving each record and destroying it individually.
1721 
1722     You can pass either a single `recordType` or an array of `recordType`s. If
1723     you pass a single `recordType`, then the record type will be used for each
1724     record.  If you pass an array, then each id must have a matching record
1725     type in the array.
1726 
1727     You can optionally pass an array of `storeKey`s instead of the `recordType`
1728     and ids.  In this case the first two parameters will be ignored.  This
1729     is usually only used by low-level internal methods.
1730 
1731     @param {SC.Record|Array} recordTypes class or array of classes
1732     @param {Array} ids ids to destroy
1733     @param {Array} storeKeys (optional) store keys to destroy
1734     @returns {SC.Store} receiver
1735   */
1736   recordsDidChange: function(recordTypes, ids, storeKeys) {
1737      var len, isArray, idx, id, recordType, storeKey;
1738       if(storeKeys===undefined){
1739         len = ids.length;
1740         isArray = SC.typeOf(recordTypes) === SC.T_ARRAY;
1741         if (!isArray) recordType = recordTypes;
1742         for(idx=0;idx<len;idx++) {
1743           if (isArray) recordType = recordTypes[idx] || SC.Record;
1744           id = ids ? ids[idx] : undefined ;
1745           storeKey = storeKeys ? storeKeys[idx] : undefined ;
1746           this.recordDidChange(recordType, id, storeKey);
1747         }
1748       }else{
1749         len = storeKeys.length;
1750         for(idx=0;idx<len;idx++) {
1751           storeKey = storeKeys ? storeKeys[idx] : undefined ;
1752           this.recordDidChange(undefined, undefined, storeKey);
1753         }
1754       }
1755       return this ;
1756   },
1757 
1758   /**
1759     Retrieves a set of records from the server.  If the records has
1760     already been loaded in the store, then this method will simply return.
1761     Otherwise if your store has a `dataSource`, this will call the
1762     `dataSource` to retrieve the record.  Generally you will not need to
1763     call this method yourself. Instead you can just use `find()`.
1764 
1765     This will not actually create a record instance but it will initiate a
1766     load of the record from the server.  You can subsequently get a record
1767     instance itself using `materializeRecord()`.
1768 
1769     @param {SC.Record|Array} recordTypes class or array of classes
1770     @param {Array} ids ids to retrieve
1771     @param {Array} storeKeys (optional) store keys to retrieve
1772     @param {Boolean} isRefresh
1773     @param {Function|Array} callback function or array of functions
1774     @returns {Array} storeKeys to be retrieved
1775   */
1776   retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh, callbacks) {
1777 
1778     var source  = this._getDataSource(),
1779         isArray = SC.typeOf(recordTypes) === SC.T_ARRAY,
1780         hasCallbackArray = SC.typeOf(callbacks) === SC.T_ARRAY,
1781         len     = (!storeKeys) ? ids.length : storeKeys.length,
1782         ret     = [],
1783         rev     = SC.Store.generateStoreKey(),
1784         K       = SC.Record,
1785         recordType, idx, storeKey, status, ok, callback;
1786 
1787     if (!isArray) recordType = recordTypes;
1788 
1789     // if no storeKeys were passed, map recordTypes + ids
1790     for(idx=0;idx<len;idx++) {
1791 
1792       // collect store key
1793       if (storeKeys) {
1794         storeKey = storeKeys[idx];
1795       } else {
1796         if (isArray) recordType = recordTypes[idx];
1797         storeKey = recordType.storeKeyFor(ids[idx]);
1798       }
1799       //collect the callback
1800       callback = hasCallbackArray ? callbacks[idx] : callbacks;
1801 
1802       // collect status and process
1803       status = this.readStatus(storeKey);
1804 
1805       // K.EMPTY, K.ERROR, K.DESTROYED_CLEAN - initial retrieval
1806       if ((status === K.EMPTY) || (status === K.ERROR) || (status === K.DESTROYED_CLEAN)) {
1807         this.writeStatus(storeKey, K.BUSY_LOADING);
1808         this.dataHashDidChange(storeKey, rev, YES);
1809         ret.push(storeKey);
1810         this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys);
1811       // otherwise, ignore record unless isRefresh is YES.
1812       } else if (isRefresh) {
1813         // K.READY_CLEAN, K.READY_DIRTY, ignore K.READY_NEW
1814         if (status & K.READY) {
1815           this.writeStatus(storeKey, K.BUSY_REFRESH | (status & 0x03)) ;
1816           this.dataHashDidChange(storeKey, rev, YES);
1817           ret.push(storeKey);
1818           this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys);
1819         // K.BUSY_DESTROYING, K.BUSY_COMMITTING, K.BUSY_CREATING
1820         } else if ((status === K.BUSY_DESTROYING) || (status === K.BUSY_CREATING) || (status === K.BUSY_COMMITTING)) {
1821           K.BUSY_ERROR.throw();
1822 
1823         // K.DESTROY_DIRTY, bad state...
1824         } else if (status === K.DESTROYED_DIRTY) {
1825           K.BAD_STATE_ERROR.throw();
1826 
1827         // ignore K.BUSY_LOADING, K.BUSY_REFRESH_CLEAN, K.BUSY_REFRESH_DIRTY
1828         }
1829       }
1830     }
1831 
1832     // now retrieve storeKeys from dataSource.  if there is no dataSource,
1833     // then act as if we couldn't retrieve.
1834     ok = NO;
1835     if (source) ok = source.retrieveRecords.call(source, this, ret, ids);
1836 
1837     // if the data source could not retrieve or if there is no source, then
1838     // simulate the data source calling dataSourceDidError on those we are
1839     // loading for the first time or dataSourceDidComplete on refreshes.
1840     if (!ok) {
1841       len = ret.length;
1842       rev = SC.Store.generateStoreKey();
1843       for(idx=0;idx<len;idx++) {
1844         storeKey = ret[idx];
1845         status   = this.readStatus(storeKey);
1846         if (status === K.BUSY_LOADING) {
1847           this.writeStatus(storeKey, K.ERROR);
1848           this.dataHashDidChange(storeKey, rev, YES);
1849 
1850         } else if (status & K.BUSY_REFRESH) {
1851           this.writeStatus(storeKey, K.READY | (status & 0x03));
1852           this.dataHashDidChange(storeKey, rev, YES);
1853         }
1854       }
1855       ret.length = 0 ; // truncate to indicate that none could refresh
1856     }
1857     return ret ;
1858   },
1859 
1860   _TMP_RETRIEVE_ARRAY: [],
1861 
1862   _callback_queue: {},
1863 
1864   /**
1865     @private
1866     stores the callbacks for the storeKeys that are inflight
1867   **/
1868   _setCallbackForStoreKey: function(storeKey, callback, hasCallbackArray, storeKeys){
1869     var queue = this._callback_queue;
1870     if(hasCallbackArray) queue[storeKey] = {callback: callback, otherKeys: storeKeys};
1871     else queue[storeKey] = callback;
1872   },
1873 
1874   /**
1875     @private
1876     Retrieves and calls callback for `storeKey` if exists, also handles if a single callback is
1877     needed for one key..
1878   **/
1879   _retrieveCallbackForStoreKey: function(storeKey){
1880     var queue = this._callback_queue,
1881         callback = queue[storeKey],
1882         allFinished, keys;
1883     if(callback){
1884       if(SC.typeOf(callback) === SC.T_FUNCTION){
1885         callback.call(); //args?
1886         delete queue[storeKey]; //cleanup
1887       }
1888       else if(SC.typeOf(callback) === SC.T_HASH){
1889         callback.completed = YES;
1890         keys = callback.storeKeys;
1891         keys.forEach(function(key){
1892           if(!queue[key].completed) allFinished = YES;
1893         });
1894         if(allFinished){
1895           callback.callback.call(); // args?
1896           //cleanup
1897           keys.forEach(function(key){
1898             delete queue[key];
1899           });
1900         }
1901 
1902       }
1903     }
1904   },
1905 
1906   /*
1907     @private
1908 
1909   */
1910   _cancelCallback: function(storeKey){
1911     var queue = this._callback_queue;
1912     if(queue[storeKey]){
1913       delete queue[storeKey];
1914     }
1915   },
1916 
1917 
1918   /**
1919     Retrieves a record from the server.  If the record has already been loaded
1920     in the store, then this method will simply return.  Otherwise if your
1921     store has a `dataSource`, this will call the `dataSource` to retrieve the
1922     record.  Generally you will not need to call this method yourself.
1923     Instead you can just use `find()`.
1924 
1925     This will not actually create a record instance but it will initiate a
1926     load of the record from the server.  You can subsequently get a record
1927     instance itself using `materializeRecord()`.
1928 
1929     @param {SC.Record} recordType class
1930     @param {String} id id to retrieve
1931     @param {Number} storeKey (optional) store key
1932     @param {Boolean} isRefresh
1933     @param {Function} callback (optional)
1934     @returns {Number} storeKey that was retrieved
1935   */
1936   retrieveRecord: function(recordType, id, storeKey, isRefresh, callback) {
1937     var array = this._TMP_RETRIEVE_ARRAY,
1938         ret;
1939 
1940     if (storeKey) {
1941       array[0] = storeKey;
1942       storeKey = array;
1943       id = null ;
1944     } else {
1945       array[0] = id;
1946       id = array;
1947     }
1948 
1949     ret = this.retrieveRecords(recordType, id, storeKey, isRefresh, callback);
1950     array.length = 0 ;
1951     return ret[0];
1952   },
1953 
1954   /**
1955     Refreshes a record from the server.  If the record has already been loaded
1956     in the store, then this method will request a refresh from the
1957     `dataSource`. Otherwise it will attempt to retrieve the record.
1958 
1959     @param {SC.Record} recordType the expected record type
1960     @param {String} id to id of the record to load
1961     @param {Number} storeKey (optional) optional store key
1962     @param {Function} callback (optional) when refresh completes
1963     @returns {Boolean} YES if the retrieval was a success.
1964   */
1965   refreshRecord: function(recordType, id, storeKey, callback) {
1966     return !!this.retrieveRecord(recordType, id, storeKey, YES, callback);
1967   },
1968 
1969   /**
1970     Refreshes a set of records from the server.  If the records has already been loaded
1971     in the store, then this method will request a refresh from the
1972     `dataSource`. Otherwise it will attempt to retrieve them.
1973 
1974     @param {SC.Record|Array} recordTypes class or array of classes
1975     @param {Array} ids ids to destroy
1976     @param {Array} storeKeys (optional) store keys to destroy
1977     @param {Function} callback (optional) when refresh completes
1978     @returns {Boolean} YES if the retrieval was a success.
1979   */
1980   refreshRecords: function(recordTypes, ids, storeKeys, callback) {
1981     var ret = this.retrieveRecords(recordTypes, ids, storeKeys, YES, callback);
1982     return ret && ret.length>0;
1983   },
1984 
1985   /**
1986     Commits the passed store keys or ids. If no `storeKey`s are given,
1987     it will commit any records in the changelog.
1988 
1989     Based on the current state of the record, this will ask the data
1990     source to perform the appropriate actions
1991     on the store keys.
1992 
1993     @param {Array} recordTypes the expected record types (SC.Record)
1994     @param {Array} ids to commit
1995     @param {SC.Set} storeKeys to commit
1996     @param {Hash} params optional additional parameters to pass along to the
1997       data source
1998     @param {Function|Array} callback function or array of callbacks
1999 
2000     @returns {Boolean} if the action was succesful.
2001   */
2002   commitRecords: function(recordTypes, ids, storeKeys, params, callbacks) {
2003     var source    = this._getDataSource(),
2004         isArray   = SC.typeOf(recordTypes) === SC.T_ARRAY,
2005         hasCallbackArray = SC.typeOf(callbacks) === SC.T_ARRAY,
2006         retCreate= [], retUpdate= [], retDestroy = [],
2007         rev       = SC.Store.generateStoreKey(),
2008         K         = SC.Record,
2009         recordType, idx, storeKey, status, ret, len, callback;
2010 
2011     // If no params are passed, look up storeKeys in the changelog property.
2012     // Remove any committed records from changelog property.
2013 
2014     if(!recordTypes && !ids && !storeKeys){
2015       storeKeys = this.changelog;
2016     }
2017 
2018     len = storeKeys ? storeKeys.get('length') : (ids ? ids.get('length') : 0);
2019 
2020     for(idx=0;idx<len;idx++) {
2021 
2022       // collect store key
2023       if (storeKeys) {
2024         storeKey = storeKeys[idx];
2025       } else {
2026         if (isArray) recordType = recordTypes[idx] || SC.Record;
2027         else recordType = recordTypes;
2028         storeKey = recordType.storeKeyFor(ids[idx]);
2029       }
2030 
2031       //collect the callback
2032       callback = hasCallbackArray ? callbacks[idx] : callbacks;
2033 
2034       // collect status and process
2035       status = this.readStatus(storeKey);
2036 
2037       if (status === K.ERROR) {
2038         K.NOT_FOUND_ERROR.throw();
2039       }
2040       else {
2041         if(status === K.READY_NEW) {
2042           this.writeStatus(storeKey, K.BUSY_CREATING);
2043           this.dataHashDidChange(storeKey, rev, YES);
2044           retCreate.push(storeKey);
2045           this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys);
2046         } else if (status === K.READY_DIRTY) {
2047           this.writeStatus(storeKey, K.BUSY_COMMITTING);
2048           this.dataHashDidChange(storeKey, rev, YES);
2049           retUpdate.push(storeKey);
2050           this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys);
2051         } else if (status === K.DESTROYED_DIRTY) {
2052           this.writeStatus(storeKey, K.BUSY_DESTROYING);
2053           this.dataHashDidChange(storeKey, rev, YES);
2054           retDestroy.push(storeKey);
2055           this._setCallbackForStoreKey(storeKey, callback, hasCallbackArray, storeKeys);
2056         } else if (status === K.DESTROYED_CLEAN) {
2057           this.dataHashDidChange(storeKey, rev, YES);
2058         }
2059         // ignore K.EMPTY, K.READY_CLEAN, K.BUSY_LOADING, K.BUSY_CREATING, K.BUSY_COMMITTING,
2060         // K.BUSY_REFRESH_CLEAN, K_BUSY_REFRESH_DIRTY, KBUSY_DESTROYING
2061       }
2062     }
2063 
2064     // now commit storeKeys to dataSource
2065     if (source && (len>0 || params)) {
2066       ret = source.commitRecords.call(source, this, retCreate, retUpdate, retDestroy, params);
2067     }
2068 
2069     //remove all committed changes from changelog
2070     if (ret && !recordTypes && !ids) {
2071       if (storeKeys === this.changelog) {
2072         this.changelog = null;
2073       }
2074       else {
2075         this.changelog.removeEach(storeKeys);
2076       }
2077     }
2078     return ret ;
2079   },
2080 
2081   /**
2082     Commits the passed store key or id.  Based on the current state of the
2083     record, this will ask the data source to perform the appropriate action
2084     on the store key.
2085 
2086     You have to pass either the id or the storeKey otherwise it will return
2087     NO.
2088 
2089     @param {SC.Record} recordType the expected record type
2090     @param {String} id the id of the record to commit
2091     @param {Number} storeKey the storeKey of the record to commit
2092     @param {Hash} params optional additional params that will passed down
2093       to the data source
2094     @param {Function|Array} callback function or array of functions
2095     @returns {Boolean} if the action was successful.
2096   */
2097   commitRecord: function(recordType, id, storeKey, params, callback) {
2098     var array = this._TMP_RETRIEVE_ARRAY,
2099         ret ;
2100     if (id === undefined && storeKey === undefined ) return NO;
2101     if (storeKey !== undefined) {
2102       array[0] = storeKey;
2103       storeKey = array;
2104       id = null ;
2105     } else {
2106       array[0] = id;
2107       id = array;
2108     }
2109 
2110     ret = this.commitRecords(recordType, id, storeKey, params, callback);
2111     array.length = 0 ;
2112     return ret;
2113   },
2114 
2115   /**
2116     Cancels an inflight request for the passed records.  Depending on the
2117     server implementation, this could cancel an entire request, causing
2118     other records to also transition their current state.
2119 
2120     @param {SC.Record|Array} recordTypes class or array of classes
2121     @param {Array} ids ids to destroy
2122     @param {Array} storeKeys (optional) store keys to destroy
2123     @returns {SC.Store} the store.
2124   */
2125   cancelRecords: function(recordTypes, ids, storeKeys) {
2126     var source  = this._getDataSource(),
2127         isArray = SC.typeOf(recordTypes) === SC.T_ARRAY,
2128         K       = SC.Record,
2129         ret     = [],
2130         status, len, idx, id, recordType, storeKey;
2131 
2132     len = (storeKeys === undefined) ? ids.length : storeKeys.length;
2133     for(idx=0;idx<len;idx++) {
2134       if (isArray) recordType = recordTypes[idx] || SC.Record;
2135       else recordType = recordTypes || SC.Record;
2136 
2137       id = ids ? ids[idx] : undefined ;
2138 
2139       if(storeKeys===undefined){
2140         storeKey = recordType.storeKeyFor(id);
2141       }else{
2142         storeKey = storeKeys ? storeKeys[idx] : undefined ;
2143       }
2144       if(storeKey) {
2145         status = this.readStatus(storeKey);
2146 
2147         if ((status === K.EMPTY) || (status === K.ERROR)) {
2148           K.NOT_FOUND_ERROR.throw();
2149         }
2150         ret.push(storeKey);
2151         this._cancelCallback(storeKey);
2152       }
2153     }
2154 
2155     if (source) source.cancel.call(source, this, ret);
2156 
2157     return this ;
2158   },
2159 
2160   /**
2161     Cancels an inflight request for the passed record.  Depending on the
2162     server implementation, this could cancel an entire request, causing
2163     other records to also transition their current state.
2164 
2165     @param {SC.Record|Array} recordTypes class or array of classes
2166     @param {Array} ids ids to destroy
2167     @param {Array} storeKeys (optional) store keys to destroy
2168     @returns {SC.Store} the store.
2169   */
2170   cancelRecord: function(recordType, id, storeKey) {
2171     var array = this._TMP_RETRIEVE_ARRAY,
2172         ret ;
2173 
2174     if (storeKey !== undefined) {
2175       array[0] = storeKey;
2176       storeKey = array;
2177       id = null ;
2178     } else {
2179       array[0] = id;
2180       id = array;
2181     }
2182 
2183     ret = this.cancelRecords(recordType, id, storeKey);
2184     array.length = 0 ;
2185     return this;
2186   },
2187 
2188   /**
2189     Convenience method can be called by the store or other parts of your
2190     application to load a record into the store.  This method will take a
2191     recordType and a data hashes and either add or update the
2192     record in the store.
2193 
2194     The loaded records will be in an `SC.Record.READY_CLEAN` state, indicating
2195     they were loaded from the data source and do not need to be committed
2196     back before changing.
2197 
2198     This method will check the state of the storeKey and call either
2199     `pushRetrieve()` or `dataSourceDidComplete()`.  The standard state constraints
2200     for these methods apply here.
2201 
2202     The return value will be the `storeKey` used for the push.  This is often
2203     convenient to pass into `loadQuery()`, if you are fetching a remote query.
2204 
2205     If you are upgrading from a pre SproutCore 1.0 application, this method
2206     is the closest to the old `updateRecord()`.
2207 
2208     @param {SC.Record} recordType the record type
2209     @param {Array} dataHash to update
2210     @param {Array} id optional.  if not passed lookup on the hash
2211     @returns {String} store keys assigned to these id
2212   */
2213   loadRecord: function(recordType, dataHash, id) {
2214     var K       = SC.Record,
2215         ret, primaryKey, storeKey;
2216 
2217     // save lookup info
2218     recordType = recordType || SC.Record;
2219     primaryKey = recordType.prototype.primaryKey;
2220 
2221 
2222     // push each record
2223     id = id || dataHash[primaryKey];
2224     ret = storeKey = recordType.storeKeyFor(id); // needed to cache
2225 
2226     if (this.readStatus(storeKey) & K.BUSY) {
2227       this.dataSourceDidComplete(storeKey, dataHash, id);
2228     } else this.pushRetrieve(recordType, id, dataHash, storeKey);
2229 
2230     // return storeKey
2231     return ret ;
2232   },
2233 
2234   /**
2235     Convenience method can be called by the store or other parts of your
2236     application to load records into the store.  This method will take a
2237     recordType and an array of data hashes and either add or update the
2238     record in the store.
2239 
2240     The loaded records will be in an `SC.Record.READY_CLEAN` state, indicating
2241     they were loaded from the data source and do not need to be committed
2242     back before changing.
2243 
2244     This method will check the state of each storeKey and call either
2245     `pushRetrieve()` or `dataSourceDidComplete()`.  The standard state
2246     constraints for these methods apply here.
2247 
2248     The return value will be the storeKeys used for each push.  This is often
2249     convenient to pass into `loadQuery()`, if you are fetching a remote query.
2250 
2251     If you are upgrading from a pre SproutCore 1.0 application, this method
2252     is the closest to the old `updateRecords()`.
2253 
2254     @param {SC.Record} recordTypes the record type or array of record types
2255     @param {Array} dataHashes array of data hashes to update
2256     @param {Array} [ids] array of ids.  if not passed lookup on hashes
2257     @returns {Array} store keys assigned to these ids
2258   */
2259   // TODO: No reason for first argument to be an array. The developer can just call loadRecords multiple times with different record type each time. Would save us the need to check if recordTypes is an Array or not.
2260   loadRecords: function (recordTypes, dataHashes, ids) {
2261     var isArray = SC.typeOf(recordTypes) === SC.T_ARRAY,
2262         len     = dataHashes.get('length'),
2263         ret     = [],
2264         recordType,
2265         id, primaryKey, idx, dataHash;
2266 
2267     // save lookup info
2268     if (!isArray) {
2269       recordType = recordTypes || SC.Record;
2270       primaryKey = recordType.prototype.primaryKey;
2271     }
2272 
2273     // push each record
2274     for (idx = 0; idx < len; idx++) {
2275       dataHash = dataHashes.objectAt(idx);
2276       if (isArray) {
2277         recordType = recordTypes.objectAt(idx) || SC.Record;
2278         primaryKey = recordType.prototype.primaryKey ;
2279       }
2280 
2281       id = (ids) ? ids.objectAt(idx) : dataHash[primaryKey];
2282 
2283       ret[idx] = this.loadRecord(recordType, dataHash, id);
2284     }
2285 
2286     // return storeKeys
2287     return ret;
2288   },
2289 
2290   /**
2291     Returns the `SC.Error` object associated with a specific record.
2292 
2293     @param {Number} storeKey The store key of the record.
2294 
2295     @returns {SC.Error} SC.Error or undefined if no error associated with the record.
2296   */
2297   readError: function(storeKey) {
2298     var errors = this.recordErrors ;
2299     return errors ? errors[storeKey] : undefined ;
2300   },
2301 
2302   /**
2303     Returns the `SC.Error` object associated with a specific query.
2304 
2305     @param {SC.Query} query The SC.Query with which the error is associated.
2306 
2307     @returns {SC.Error} SC.Error or undefined if no error associated with the query.
2308   */
2309   readQueryError: function(query) {
2310     var errors = this.queryErrors ;
2311     return errors ? errors[SC.guidFor(query)] : undefined ;
2312   },
2313 
2314   // ..........................................................
2315   // DATA SOURCE CALLBACKS
2316   //
2317   // Mathods called by the data source on the store
2318 
2319   /**
2320     Called by a `dataSource` when it cancels an inflight operation on a
2321     record.  This will transition the record back to it non-inflight state.
2322 
2323     @param {Number} storeKey record store key to cancel
2324     @returns {SC.Store} receiver
2325   */
2326   dataSourceDidCancel: function(storeKey) {
2327     var status = this.readStatus(storeKey),
2328         K      = SC.Record;
2329 
2330     // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN,
2331     // DESTROYED_DIRTY
2332     if (!(status & K.BUSY)) {
2333       K.BAD_STATE_ERROR.throw(); // should never be called in this state
2334     }
2335 
2336     // otherwise, determine proper state transition
2337     switch(status) {
2338       case K.BUSY_LOADING:
2339         status = K.EMPTY;
2340         break ;
2341 
2342       case K.BUSY_CREATING:
2343         status = K.READY_NEW;
2344         break;
2345 
2346       case K.BUSY_COMMITTING:
2347         status = K.READY_DIRTY ;
2348         break;
2349 
2350       case K.BUSY_REFRESH_CLEAN:
2351         status = K.READY_CLEAN;
2352         break;
2353 
2354       case K.BUSY_REFRESH_DIRTY:
2355         status = K.READY_DIRTY ;
2356         break ;
2357 
2358       case K.BUSY_DESTROYING:
2359         status = K.DESTROYED_DIRTY ;
2360         break;
2361 
2362       default:
2363         K.BAD_STATE_ERROR.throw() ;
2364     }
2365     this.writeStatus(storeKey, status) ;
2366     this.dataHashDidChange(storeKey, null, YES);
2367     this._cancelCallback(storeKey);
2368 
2369     return this ;
2370   },
2371 
2372   /**
2373     Called by a data source when it creates or commits a record.  Passing an
2374     optional id will remap the `storeKey` to the new record id.  This is
2375     required when you commit a record that does not have an id yet.
2376 
2377     @param {Number} storeKey record store key to change to READY_CLEAN state
2378     @param {Hash} dataHash optional data hash to replace current hash
2379     @param {Object} newId optional new id to replace the old one
2380     @returns {SC.Store} receiver
2381   */
2382   dataSourceDidComplete: function(storeKey, dataHash, newId) {
2383     var status = this.readStatus(storeKey), K = SC.Record, statusOnly;
2384 
2385     // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN,
2386     // DESTROYED_DIRTY
2387     if (!(status & K.BUSY)) {
2388       K.BAD_STATE_ERROR.throw(); // should never be called in this state
2389     }
2390 
2391     // otherwise, determine proper state transition
2392     if(status === K.BUSY_DESTROYING) {
2393       K.BAD_STATE_ERROR.throw();
2394     } else status = K.READY_CLEAN;
2395 
2396     this.writeStatus(storeKey, status);
2397     if (dataHash) this.writeDataHash(storeKey, dataHash, status);
2398     if (newId) { SC.Store.replaceIdFor(storeKey, newId); }
2399 
2400     statusOnly = dataHash || newId ? NO : YES;
2401     this.dataHashDidChange(storeKey, null, statusOnly);
2402 
2403     // Force record to refresh its cached properties based on store key
2404     var record = this.materializeRecord(storeKey);
2405     if (record !== null) {
2406       // If the record's id property has been computed, ensure that it re-computes.
2407       if (newId) { record.propertyDidChange('id'); }
2408       record.notifyPropertyChange('status');
2409     }
2410     //update callbacks
2411     this._retrieveCallbackForStoreKey(storeKey);
2412 
2413     return this ;
2414   },
2415 
2416   /**
2417     Called by a data source when it has destroyed a record.  This will
2418     transition the record to the proper state.
2419 
2420     @param {Number} storeKey record store key to cancel
2421     @returns {SC.Store} receiver
2422   */
2423   dataSourceDidDestroy: function(storeKey) {
2424     var status = this.readStatus(storeKey), K = SC.Record;
2425 
2426     // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN,
2427     // DESTROYED_DIRTY
2428     if (!(status & K.BUSY)) {
2429       K.BAD_STATE_ERROR.throw(); // should never be called in this state
2430     }
2431     // otherwise, determine proper state transition
2432     else{
2433       status = K.DESTROYED_CLEAN ;
2434     }
2435     this.removeDataHash(storeKey, status) ;
2436     this.dataHashDidChange(storeKey);
2437 
2438     // Force record to refresh its cached properties based on store key
2439     var record = this.materializeRecord(storeKey);
2440     if (record !== null) {
2441       record.notifyPropertyChange('status');
2442     }
2443 
2444     this._retrieveCallbackForStoreKey(storeKey);
2445 
2446     return this ;
2447   },
2448 
2449   /**
2450     Converts the passed record into an error object.
2451 
2452     @param {Number} storeKey record store key to error
2453     @param {SC.Error} error [optional] an SC.Error instance to associate with storeKey
2454     @returns {SC.Store} receiver
2455   */
2456   dataSourceDidError: function(storeKey, error) {
2457     var status = this.readStatus(storeKey), errors = this.recordErrors, K = SC.Record;
2458 
2459     // EMPTY, ERROR, READY_CLEAN, READY_NEW, READY_DIRTY, DESTROYED_CLEAN,
2460     // DESTROYED_DIRTY
2461     if (!(status & K.BUSY)) { K.BAD_STATE_ERROR.throw(); }
2462 
2463     // otherwise, determine proper state transition
2464     else status = K.ERROR ;
2465 
2466     // Add the error to the array of record errors (for lookup later on if necessary).
2467     if (error && error.isError) {
2468       if (!errors) errors = this.recordErrors = [];
2469       errors[storeKey] = error;
2470     }
2471 
2472     this.writeStatus(storeKey, status) ;
2473     this.dataHashDidChange(storeKey, null, YES);
2474 
2475     // Force record to refresh its cached properties based on store key
2476     var record = this.materializeRecord(storeKey);
2477     if (record) {
2478       record.notifyPropertyChange('status');
2479     }
2480 
2481     this._retrieveCallbackForStoreKey(storeKey);
2482     return this ;
2483   },
2484 
2485   // ..........................................................
2486   // PUSH CHANGES FROM DATA SOURCE
2487   //
2488 
2489   /**
2490     Call by the data source whenever you want to push new data out of band
2491     into the store.
2492 
2493     @param {Class} recordType the SC.Record subclass
2494     @param {Object} id the record id or null
2495     @param {Hash} dataHash data hash to load
2496     @param {Number} storeKey optional store key.
2497     @returns {Number|Boolean} storeKey if push was allowed, NO if not
2498   */
2499   pushRetrieve: function(recordType, id, dataHash, storeKey) {
2500     var K = SC.Record, status;
2501 
2502     if(storeKey===undefined) storeKey = recordType.storeKeyFor(id);
2503     status = this.readStatus(storeKey);
2504     if(status === K.EMPTY || status === K.ERROR || status === K.READY_CLEAN || status === K.DESTROYED_CLEAN) {
2505 
2506       status = K.READY_CLEAN;
2507       if(dataHash === undefined) this.writeStatus(storeKey, status) ;
2508       else this.writeDataHash(storeKey, dataHash, status) ;
2509 
2510       if (id && this.idFor(storeKey) !== id) {
2511         SC.Store.replaceIdFor(storeKey, id);
2512 
2513         // If the record's id property has been computed, ensure that it re-computes.
2514         var record = this.materializeRecord(storeKey);
2515         record.propertyDidChange('id');
2516       }
2517       this.dataHashDidChange(storeKey);
2518 
2519       return storeKey;
2520     }
2521     //conflicted (ready)
2522     return NO;
2523   },
2524 
2525   /**
2526     Call by the data source whenever you want to push a deletion into the
2527     store.
2528 
2529     @param {Class} recordType the SC.Record subclass
2530     @param {Object} id the record id or null
2531     @param {Number} storeKey optional store key.
2532     @returns {Number|Boolean} storeKey if push was allowed, NO if not
2533   */
2534   pushDestroy: function(recordType, id, storeKey) {
2535     var K = SC.Record, status;
2536 
2537     if(storeKey===undefined){
2538       storeKey = recordType.storeKeyFor(id);
2539     }
2540     status = this.readStatus(storeKey);
2541     if(status === K.EMPTY || status === K.ERROR || status === K.READY_CLEAN || status === K.DESTROYED_CLEAN){
2542       status = K.DESTROYED_CLEAN;
2543       this.removeDataHash(storeKey, status) ;
2544       this.dataHashDidChange(storeKey);
2545       return storeKey;
2546     }
2547     //conflicted (destroy)
2548     return NO;
2549   },
2550 
2551   /**
2552     Call by the data source whenever you want to push an error into the
2553     store.
2554 
2555     @param {Class} recordType the SC.Record subclass
2556     @param {Object} id the record id or null
2557     @param {SC.Error} error [optional] an SC.Error instance to associate with id or storeKey
2558     @param {Number} storeKey optional store key.
2559     @returns {Number|Boolean} storeKey if push was allowed, NO if not
2560   */
2561   pushError: function(recordType, id, error, storeKey) {
2562     var K = SC.Record, status, errors = this.recordErrors;
2563 
2564     if(storeKey===undefined) storeKey = recordType.storeKeyFor(id);
2565     status = this.readStatus(storeKey);
2566 
2567     if(status === K.EMPTY || status === K.ERROR || status === K.READY_CLEAN || status === K.DESTROYED_CLEAN){
2568       status = K.ERROR;
2569 
2570       // Add the error to the array of record errors (for lookup later on if necessary).
2571       if (error && error.isError) {
2572         if (!errors) errors = this.recordErrors = [];
2573         errors[storeKey] = error;
2574       }
2575 
2576       this.writeStatus(storeKey, status) ;
2577       this.dataHashDidChange(storeKey, null, YES);
2578       return storeKey;
2579     }
2580     //conflicted (error)
2581     return NO;
2582   },
2583 
2584   // ..........................................................
2585   // FETCH CALLBACKS
2586   //
2587 
2588   // **NOTE**: although these method works on RecordArray instances right now.
2589   // They could be optimized to actually share query results between nested
2590   // stores.  This is why these methods are implemented here instead of
2591   // directly on `Query` or `RecordArray` objects.
2592 
2593   /** @deprecated
2594 
2595     @param {SC.Query} query the query you are loading.  must be remote.
2596     @param {SC.Array} storeKeys array of store keys
2597     @returns {SC.Store} receiver
2598   */
2599   loadQueryResults: function(query, storeKeys) {
2600     //@if(debug)
2601     if (query.get('location') === SC.Query.LOCAL) {
2602       throw new Error("Developer Error: You should not call loadQueryResults with a local query.  You need to use dataSourceDidFetchQuery instead.");
2603     } else {
2604       SC.warn("Developer Warning: loadQueryResults has been deprecated in favor of using dataSourceDidFetchQuery for both local and remote queries.  With remote queries, include the store keys when calling dataSourceDidFetchQuery.");
2605     }
2606     //@endif
2607 
2608     return this.dataSourceDidFetchQuery(query, storeKeys);
2609   },
2610 
2611   /**
2612     Called by your data source whenever you finish fetching the results of a
2613     query.  This will put the record array for the query into a READY_CLEAN
2614     state if it was previously loading or refreshing.
2615 
2616     # Handling REMOTE queries
2617 
2618     Note that if the query is REMOTE, then you must first load the results
2619     into the store using `loadRecords()` and pass the ordered array of store
2620     keys returned by `loadRecords()` into this method.
2621 
2622     For example,
2623 
2624         storeKeys = store.loadRecords(MyApp.SomeType, body.contacts);
2625         store.dataSourceDidFetchQuery(query, storeKeys);
2626 
2627     # Automatic updates
2628 
2629     When you call this method the record array for the query will notify that
2630     its contents have changed.  If the query is LOCAL then the contents will
2631     update automatically to include any new records you added to the store.
2632     If the query is REMOTE the contents will update to be the ordered records
2633     for the passed in store keys.
2634 
2635     # Incremental loading for REMOTE queries
2636 
2637     If you want to support incremental loading, then pass an SC.SparseArray
2638     object to hold the store keys.  This will allow you to load results
2639     incrementally and provide more store keys as you do.
2640 
2641     See the SC.SparseArray documentation for more information.
2642 
2643     @param {SC.Query} query The query you fetched
2644     @param {Array} [storeKeys] Ordered array of store keys as returned by a remote query.  NOTE: Required for remote queries.
2645     @returns {SC.Store} receiver
2646   */
2647   dataSourceDidFetchQuery: function (query, storeKeys) {
2648     var recArray = this._findQuery(query, YES, NO);
2649 
2650     // Set the ordered array of store keys for remote queries.
2651     if (recArray && query.get('isRemote')) {
2652       //@if(debug)
2653       // Prevent confusion between local and remote requests.
2654       if (SC.none(storeKeys)) {
2655         throw new Error("Developer Error: The storeKeys argument in dataSourceDidFetchQuery is not optional for remote queries.  For a remote query you must include the ordered array of store keys for the loaded records (even if it's an empty array).");
2656       }
2657       //@endif
2658 
2659       recArray.set('storeKeys', storeKeys);
2660     }
2661 
2662     return this._scstore_dataSourceDidFetchQuery(query);
2663   },
2664 
2665   /** @private */
2666   _scstore_dataSourceDidFetchQuery: function (query) {
2667     var recArray     = this._findQuery(query, NO, NO),
2668         nestedStores = this.get('nestedStores'),
2669         loc          = nestedStores ? nestedStores.get('length') : 0;
2670 
2671     // fix query if needed
2672     if (recArray) recArray.storeDidFetchQuery(query);
2673 
2674     // notify nested stores
2675     while(--loc >= 0) {
2676       nestedStores[loc]._scstore_dataSourceDidFetchQuery(query);
2677     }
2678 
2679     return this ;
2680   },
2681 
2682   /**
2683     Called by your data source if it cancels fetching the results of a query.
2684     This will put any RecordArray's back into its original state (READY or
2685     EMPTY).
2686 
2687     @param {SC.Query} query the query you cancelled
2688     @returns {SC.Store} receiver
2689   */
2690   dataSourceDidCancelQuery: function(query) {
2691     return this._scstore_dataSourceDidCancelQuery(query, YES);
2692   },
2693 
2694   _scstore_dataSourceDidCancelQuery: function(query, createIfNeeded) {
2695     var recArray     = this._findQuery(query, createIfNeeded, NO),
2696         nestedStores = this.get('nestedStores'),
2697         loc          = nestedStores ? nestedStores.get('length') : 0;
2698 
2699     // fix query if needed
2700     if (recArray) recArray.storeDidCancelQuery(query);
2701 
2702     // notify nested stores
2703     while(--loc >= 0) {
2704       nestedStores[loc]._scstore_dataSourceDidCancelQuery(query, NO);
2705     }
2706 
2707     return this ;
2708   },
2709 
2710   /**
2711     Called by your data source if it encountered an error loading the query.
2712     This will put the query into an error state until you try to refresh it
2713     again.
2714 
2715     @param {SC.Query} query the query with the error
2716     @param {SC.Error} error [optional] an SC.Error instance to associate with query
2717     @returns {SC.Store} receiver
2718   */
2719   dataSourceDidErrorQuery: function(query, error) {
2720     var errors = this.queryErrors;
2721 
2722     // Add the error to the array of query errors (for lookup later on if necessary).
2723     if (error && error.isError) {
2724       if (!errors) errors = this.queryErrors = {};
2725       errors[SC.guidFor(query)] = error;
2726     }
2727 
2728     return this._scstore_dataSourceDidErrorQuery(query, YES);
2729   },
2730 
2731   _scstore_dataSourceDidErrorQuery: function(query, createIfNeeded) {
2732     var recArray     = this._findQuery(query, createIfNeeded, NO),
2733         nestedStores = this.get('nestedStores'),
2734         loc          = nestedStores ? nestedStores.get('length') : 0;
2735 
2736     // fix query if needed
2737     if (recArray) recArray.storeDidErrorQuery(query);
2738 
2739     // notify nested stores
2740     while(--loc >= 0) {
2741       nestedStores[loc]._scstore_dataSourceDidErrorQuery(query, NO);
2742     }
2743 
2744     return this ;
2745   },
2746 
2747   // ..........................................................
2748   // INTERNAL SUPPORT
2749   //
2750 
2751   /** @private */
2752   init: function() {
2753     sc_super();
2754     this.reset();
2755   },
2756 
2757 
2758   toString: function() {
2759     // Include the name if the client has specified one.
2760     var name = this.get('name');
2761     if (!name) {
2762       return sc_super();
2763     }
2764     else {
2765       var ret = sc_super();
2766       return "%@ (%@)".fmt(name, ret);
2767     }
2768   },
2769 
2770 
2771   // ..........................................................
2772   // PRIMARY KEY CONVENIENCE METHODS
2773   //
2774 
2775   /**
2776     Given a `storeKey`, return the `primaryKey`.
2777 
2778     @param {Number} storeKey the store key
2779     @returns {String} primaryKey value
2780   */
2781   idFor: function(storeKey) {
2782     return SC.Store.idFor(storeKey);
2783   },
2784 
2785   /**
2786     Given a storeKey, return the recordType.
2787 
2788     @param {Number} storeKey the store key
2789     @returns {SC.Record} record instance
2790   */
2791   recordTypeFor: function(storeKey) {
2792     return SC.Store.recordTypeFor(storeKey) ;
2793   },
2794 
2795   /**
2796     Given a `recordType` and `primaryKey`, find the `storeKey`. If the
2797     `primaryKey` has not been assigned a `storeKey` yet, it will be added.
2798 
2799     @param {SC.Record} recordType the record type
2800     @param {String} primaryKey the primary key
2801     @returns {Number} storeKey
2802   */
2803   storeKeyFor: function(recordType, primaryKey) {
2804     return recordType.storeKeyFor(primaryKey);
2805   },
2806 
2807   /**
2808     Given a `primaryKey` value for the record, returns the associated
2809     `storeKey`.  As opposed to `storeKeyFor()` however, this method
2810     will **NOT** generate a new `storeKey` but returned `undefined`.
2811 
2812     @param {SC.Record} recordType the record type
2813     @param {String} primaryKey the primary key
2814     @returns {Number} a storeKey.
2815   */
2816   storeKeyExists: function(recordType, primaryKey) {
2817     return recordType.storeKeyExists(primaryKey);
2818   },
2819 
2820   /**
2821     Finds all `storeKey`s of a certain record type in this store
2822     and returns an array.
2823 
2824     @param {SC.Record} recordType
2825     @returns {Array} set of storeKeys
2826   */
2827   storeKeysFor: function(recordType) {
2828     var ret = [],
2829         isEnum = recordType && recordType.isEnumerable,
2830         recType, storeKey, isMatch ;
2831 
2832     if (!this.statuses) return ret;
2833     for(storeKey in SC.Store.recordTypesByStoreKey) {
2834       recType = SC.Store.recordTypesByStoreKey[storeKey];
2835 
2836       // if same record type and this store has it
2837       if (isEnum) isMatch = recordType.contains(recType);
2838       else isMatch = recType === recordType;
2839 
2840       if(isMatch && this.statuses[storeKey]) ret.push(parseInt(storeKey, 10));
2841     }
2842 
2843     return ret;
2844   },
2845 
2846   /**
2847     Finds all `storeKey`s in this store
2848     and returns an array.
2849 
2850     @returns {Array} set of storeKeys
2851   */
2852   storeKeys: function() {
2853     var ret = [], storeKey;
2854     if(!this.statuses) return ret;
2855 
2856     for(storeKey in this.statuses) {
2857       // if status is not empty
2858       if(this.statuses[storeKey] !== SC.Record.EMPTY) {
2859         ret.push(parseInt(storeKey, 10));
2860       }
2861     }
2862 
2863     return ret;
2864   },
2865 
2866   /**
2867     Returns string representation of a `storeKey`, with status.
2868 
2869     @param {Number} storeKey
2870     @returns {String}
2871   */
2872   statusString: function(storeKey) {
2873     var rec = this.materializeRecord(storeKey);
2874     return rec.statusString();
2875   }
2876 
2877 }) ;
2878 
2879 SC.Store.mixin(/** @scope SC.Store.prototype */{
2880 
2881   /**
2882     Standard error raised if you try to commit changes from a nested store
2883     and there is a conflict.
2884 
2885     @type Error
2886   */
2887   CHAIN_CONFLICT_ERROR: SC.$error("Nested Store Conflict"),
2888 
2889   /**
2890     Standard error if you try to perform an operation on a nested store
2891     without a parent.
2892 
2893     @type Error
2894   */
2895   NO_PARENT_STORE_ERROR: SC.$error("Parent Store Required"),
2896 
2897   /**
2898     Standard error if you try to perform an operation on a nested store that
2899     is only supported in root stores.
2900 
2901     @type Error
2902   */
2903   NESTED_STORE_UNSUPPORTED_ERROR: SC.$error("Unsupported In Nested Store"),
2904 
2905   /**
2906     Standard error if you try to retrieve a record in a nested store that is
2907     dirty.  (This is allowed on the main store, but not in nested stores.)
2908 
2909     @type Error
2910   */
2911   NESTED_STORE_RETRIEVE_DIRTY_ERROR: SC.$error("Cannot Retrieve Dirty Record in Nested Store"),
2912 
2913   /**
2914     Data hash state indicates the data hash is currently editable
2915 
2916     @type String
2917   */
2918   EDITABLE:  'editable',
2919 
2920   /**
2921     Data hash state indicates the hash no longer tracks changes from a
2922     parent store, but it is not editable.
2923 
2924     @type String
2925   */
2926   LOCKED:    'locked',
2927 
2928   /**
2929     Data hash state indicates the hash is tracking changes from the parent
2930     store and is not editable.
2931 
2932     @type String
2933   */
2934   INHERITED: 'inherited',
2935 
2936   /** @private
2937     This array maps all storeKeys to primary keys.  You will not normally
2938     access this method directly.  Instead use the `idFor()` and
2939     `storeKeyFor()` methods on `SC.Record`.
2940   */
2941   idsByStoreKey: [],
2942 
2943   /** @private
2944     Maps all `storeKey`s to a `recordType`.  Once a `storeKey` is associated
2945     with a `primaryKey` and `recordType` that remains constant throughout the
2946     lifetime of the application.
2947   */
2948   recordTypesByStoreKey: {},
2949 
2950   /** @private
2951     Maps some `storeKeys` to query instance.  Once a `storeKey` is associated
2952     with a query instance, that remains constant through the lifetime of the
2953     application.  If a `Query` is destroyed, it will remove itself from this
2954     list.
2955 
2956     Don't access this directly.  Use queryFor().
2957   */
2958   queriesByStoreKey: [],
2959 
2960   /** @private
2961     The next store key to allocate.  A storeKey must always be greater than 0
2962   */
2963   nextStoreKey: 1,
2964 
2965   /**
2966     Generates a new store key for use.
2967 
2968     @type Number
2969   */
2970   generateStoreKey: function() { return this.nextStoreKey++; },
2971 
2972   /**
2973     Given a `storeKey` returns the `primaryKey` associated with the key.
2974     If no `primaryKey` is associated with the `storeKey`, returns `null`.
2975 
2976     @param {Number} storeKey the store key
2977     @returns {String} the primary key or null
2978   */
2979   idFor: function(storeKey) {
2980     return this.idsByStoreKey[storeKey] ;
2981   },
2982 
2983   /**
2984     Given a `storeKey`, returns the query object associated with the key.  If
2985     no query is associated with the `storeKey`, returns `null`.
2986 
2987     @param {Number} storeKey the store key
2988     @returns {SC.Query} query query object
2989   */
2990   queryFor: function(storeKey) {
2991     return this.queriesByStoreKey[storeKey];
2992   },
2993 
2994   /**
2995     Given a `storeKey` returns the `SC.Record` class associated with the key.
2996     If no record type is associated with the store key, returns `null`.
2997 
2998     The SC.Record class will only be found if you have already called
2999     storeKeyFor() on the record.
3000 
3001     @param {Number} storeKey the store key
3002     @returns {SC.Record} the record type
3003   */
3004   recordTypeFor: function(storeKey) {
3005     return this.recordTypesByStoreKey[storeKey];
3006   },
3007 
3008   /**
3009     Swaps the `primaryKey` mapped to the given storeKey with the new
3010     `primaryKey`.  If the `storeKey` is not currently associated with a record
3011     this will raise an exception.
3012 
3013     @param {Number} storeKey the existing store key
3014     @param {String} newPrimaryKey the new primary key
3015     @returns {SC.Store} receiver
3016   */
3017   replaceIdFor: function(storeKey, newId) {
3018     var oldId = this.idsByStoreKey[storeKey],
3019         recordType, storeKeys;
3020 
3021     if (oldId !== newId) { // skip if id isn't changing
3022 
3023       recordType = this.recordTypeFor(storeKey);
3024        if (!recordType) {
3025         throw new Error("replaceIdFor: storeKey %@ does not exist".fmt(storeKey));
3026       }
3027 
3028       // map one direction...
3029       this.idsByStoreKey[storeKey] = newId;
3030 
3031       // then the other...
3032       storeKeys = recordType.storeKeysById() ;
3033       delete storeKeys[oldId];
3034       storeKeys[newId] = storeKey;
3035     }
3036 
3037     return this ;
3038   },
3039 
3040   /**
3041     Swaps the `recordType` recorded for a given `storeKey`.  Normally you
3042     should not call this method directly as it can damage the store behavior.
3043     This method is used by other store methods to set the `recordType` for a
3044     `storeKey`.
3045 
3046     @param {Integer} storeKey the store key
3047     @param {SC.Record} recordType a record class
3048     @returns {SC.Store} receiver
3049   */
3050   replaceRecordTypeFor: function(storeKey, recordType) {
3051     this.recordTypesByStoreKey[storeKey] = recordType;
3052     return this ;
3053   }
3054 
3055 });
3056