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