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   A `RecordArray` is a managed list of records (instances of your `SC.Record`
 14   model classes).
 15 
 16   Using RecordArrays
 17   ---
 18 
 19   Most often, RecordArrays contain the results of a `SC.Query`. You will generally not
 20   create or modify record arrays yourselves, instead using the ones returned from calls
 21   to `SC.Store#find` with either a record type or a query.
 22 
 23   The membership of these query-backed record arrays is managed by the store, which
 24   searches already-loaded records for local queries, and defers to your data source
 25   for remote ones (see `SC.Query` documentation for more details). Since membership
 26   in these record arrays is managed by the store, you will not generally add, remove
 27   or rearrange them here (see `isEditable` below).
 28 
 29   Query-backed record arrays have a status property which reflects the store's progress
 30   in fulfilling the query. See notes on `status` below.
 31 
 32   (Note that instances of `SC.Query` are dumb descriptor objects which do not have a
 33   status or results of their own. References to a query's status or results should be
 34   understood to refer to those of its record array.)
 35 
 36   Internal Notes
 37   ---
 38 
 39   This section is about `RecordArray` internals, and is only intended for those
 40   who need to extend this class to do something special.
 41 
 42   A `RecordArray` wraps an array of store keys, listed on its `storeKeys`
 43   property. If you request a record from the array, e.g. via `objectAt`,
 44   the `RecordArray` will convert the requested store key to a record
 45   suitable for public use.
 46 
 47   The list of store keys is usually managed by the store. If you are using
 48   your `RecordArray` with a query, you should manage the results via the
 49   store, and not try to directly manipulate the results via the array. If you
 50   are managing the array's store keys yourself, then any array-like operation
 51   will be translated into similar calls on the underlying `storeKeys` array.
 52   This underlying array can be a real array, or, if you wish to implement
 53   incremental loading, it may be a `SparseArray`.
 54 
 55   If the record array is created with an `SC.Query` object (as is almost always the
 56   case), then the record array will also consult the query for various delegate
 57   operations such as determining if the record array should update automatically
 58   whenever records in the store changes. It will also ask the query to refresh the
 59   `storeKeys` list whenever records change in the store.
 60 
 61   @extends SC.Object
 62   @extends SC.Enumerable
 63   @extends SC.Array
 64   @since SproutCore 1.0
 65 */
 66 
 67 SC.RecordArray = SC.Object.extend(SC.Enumerable, SC.Array,
 68   /** @scope SC.RecordArray.prototype */ {
 69 
 70   //@if(debug)
 71   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
 72 
 73   /* @private */
 74   toString: function () {
 75     var statusString = this.statusString(),
 76       storeKeys = this.get('storeKeys'),
 77       query = this.get('query'),
 78       length = this.get('length');
 79 
 80     return "%@({\n    query: %@,\n    storeKeys: [%@],\n    length: %@,\n    … }) %@".fmt(sc_super(), query, storeKeys, length, statusString);
 81   },
 82 
 83   /** @private */
 84   statusString: function() {
 85     var ret = [], status = this.get('status');
 86 
 87     for (var prop in SC.Record) {
 88       if (prop.match(/[A-Z_]$/) && SC.Record[prop] === status) {
 89         ret.push(prop);
 90       }
 91     }
 92 
 93     return ret.join(" ");
 94   },
 95 
 96   /* END DEBUG ONLY PROPERTIES AND METHODS */
 97   //@endif
 98 
 99   /**
100     The store that owns this record array.  All record arrays must have a
101     store to function properly.
102 
103     NOTE: You **MUST** set this property on the `RecordArray` when creating
104     it or else it will fail.
105 
106     @type SC.Store
107   */
108   store: null,
109 
110   /**
111     The `SC.Query` object this record array is based upon.  All record arrays
112     **MUST** have an associated query in order to function correctly.  You
113     cannot change this property once it has been set.
114 
115     NOTE: You **MUST** set this property on the `RecordArray` when creating
116     it or else it will fail.
117 
118     @type SC.Query
119   */
120   query: null,
121 
122   /**
123     The array of `storeKeys` as retrieved from the owner store.
124 
125     @type SC.Array
126   */
127   storeKeys: null,
128 
129   /** @private The cache of previous store keys so that we can avoid unnecessary updates. */
130   _prevStoreKeys: null,
131 
132   /**
133     Reflects the store's current status in fulfilling the record array's query. Note
134     that this status is not directly related to the status of the array's records:
135 
136     - The store returns local queries immediately, regardless of any first-time loading
137       they may trigger. (Note that by default, local queries are limited to 100ms
138       processing time per run loop, so very complex queries may take several run loops
139       to return to `READY` status. You can edit `SC.RecordArray.QUERY_MATCHING_THRESHOLD`
140       to change this duration.)
141     - The store fulfills remote queries by passing them to your data source's `fetch`
142       method. While fetching, it sets your array's status to `BUSY_LOADING` or
143       `BUSY_REFRESHING`. Once your data source has finished fetching (successfully or
144       otherwise), it will call the appropriate store methods (e.g. `dataSourceDidFetchQuery`
145       or `dataSourceDidErrorQuery`), which will update the query's array's status.
146 
147     Thus, a record array may have a `READY` status while records are still loading (if
148     a local query triggers a call to `SC.DataSource#fetch`), and will not reflect the
149     `DIRTY` status of any of its records.
150 
151     @type Number
152   */
153   status: SC.Record.EMPTY,
154 
155   /**
156     The current editable state of the query. If this record array is not backed by a
157     query, it is assumed to be editable.
158 
159     @property
160     @type Boolean
161   */
162   isEditable: function() {
163     var query = this.get('query');
164     return query ? query.get('isEditable') : YES;
165   }.property('query').cacheable(),
166 
167   // ..........................................................
168   // ARRAY PRIMITIVES
169   //
170 
171   /** @private
172     Returned length is a pass-through to the `storeKeys` array.
173 		@property
174   */
175   length: function() {
176     this.flush(); // cleanup pending changes
177     var storeKeys = this.get('storeKeys');
178     return storeKeys ? storeKeys.get('length') : 0;
179   }.property('storeKeys').cacheable(),
180 
181   /** @private
182     A cache of materialized records. The first time an instance of SC.Record is
183     created for a store key at a given index, it will be saved to this array.
184 
185     Whenever the `storeKeys` property is reset, this cache is also reset.
186 
187     @type Array
188   */
189   _scra_records: null,
190 
191   /** @private
192     Looks up the store key in the `storeKeys array and materializes a
193     records.
194 
195     @param {Number} idx index of the object
196     @return {SC.Record} materialized record
197   */
198   objectAt: function(idx) {
199 
200     this.flush(); // cleanup pending if needed
201 
202     var recs      = this._scra_records,
203         storeKeys = this.get('storeKeys'),
204         store     = this.get('store'),
205         storeKey, ret ;
206 
207     if (!storeKeys || !store) return undefined; // nothing to do
208     if (recs && (ret=recs[idx])) return ret ; // cached
209 
210     // not in cache, materialize
211     if (!recs) this._scra_records = recs = [] ; // create cache
212     storeKey = storeKeys.objectAt(idx);
213 
214     if (storeKey) {
215       // if record is not loaded already, then ask the data source to
216       // retrieve it
217       if (store.peekStatus(storeKey) === SC.Record.EMPTY) {
218         store.retrieveRecord(null, null, storeKey);
219       }
220       recs[idx] = ret = store.materializeRecord(storeKey);
221     }
222     return ret ;
223   },
224 
225   /** @private - optimized forEach loop. */
226   forEach: function(callback, target) {
227     this.flush();
228 
229     var recs      = this._scra_records,
230         storeKeys = this.get('storeKeys'),
231         store     = this.get('store'),
232         len       = storeKeys ? storeKeys.get('length') : 0,
233         idx, storeKey, rec;
234 
235     if (!storeKeys || !store) return this; // nothing to do
236     if (!recs) recs = this._scra_records = [] ;
237     if (!target) target = this;
238 
239     for(idx=0;idx<len;idx++) {
240       rec = recs[idx];
241       if (!rec) {
242         rec = recs[idx] = store.materializeRecord(storeKeys.objectAt(idx));
243       }
244       callback.call(target, rec, idx, this);
245     }
246 
247     return this;
248   },
249 
250   /** @private
251     Replaces a range of records starting at a given index with the replacement
252     records provided. The objects to be inserted must be instances of SC.Record
253     and must have a store key assigned to them.
254 
255     Note that most SC.RecordArrays are *not* editable via `replace()`, since they
256     are generated by a rule-based SC.Query. You can check the `isEditable` property
257     before attempting to modify a record array.
258 
259     @param {Number} idx start index
260     @param {Number} amt count of records to remove
261     @param {SC.RecordArray} recs the records that should replace the removed records
262 
263     @returns {SC.RecordArray} receiver, after mutation has occurred
264   */
265   replace: function(idx, amt, recs) {
266 
267     this.flush(); // cleanup pending if needed
268 
269     var storeKeys = this.get('storeKeys'),
270         len       = recs ? (recs.get ? recs.get('length') : recs.length) : 0,
271         i, keys;
272 
273     if (!storeKeys) throw new Error("Unable to edit an SC.RecordArray that does not have its storeKeys property set.");
274 
275     if (!this.get('isEditable')) SC.RecordArray.NOT_EDITABLE.throw();
276 
277     // map to store keys
278     keys = [] ;
279     for(i=0;i<len;i++) keys[i] = recs.objectAt(i).get('storeKey');
280 
281     // pass along - if allowed, this should trigger the content observer
282     storeKeys.replace(idx, amt, keys);
283     return this;
284   },
285 
286   /**
287     Returns YES if the passed record can be found in the record array.  This is
288     provided for compatibility with SC.Set.
289 
290     @param {SC.Record} record
291     @returns {Boolean}
292   */
293   contains: function(record) {
294     return this.indexOf(record)>=0;
295   },
296 
297   /** @private
298     Returns the first index where the specified record is found.
299 
300     @param {SC.Record} record
301     @param {Number} startAt optional starting index
302     @returns {Number} index
303   */
304   indexOf: function(record, startAt) {
305     if (!SC.kindOf(record, SC.Record)) {
306       //@if(debug)
307       SC.Logger.warn("Developer Warning: Used SC.RecordArray's `indexOf` on %@, which is not an SC.Record. SC.RecordArray only works with records.".fmt(record));
308       SC.Logger.trace();
309       //@endif
310       return -1; // only takes records
311     }
312 
313     this.flush();
314 
315     var storeKey  = record.get('storeKey'),
316         storeKeys = this.get('storeKeys');
317 
318     return storeKeys ? storeKeys.indexOf(storeKey, startAt) : -1;
319   },
320 
321   /** @private
322     Returns the last index where the specified record is found.
323 
324     @param {SC.Record} record
325     @param {Number} startAt optional starting index
326     @returns {Number} index
327   */
328   lastIndexOf: function(record, startAt) {
329     if (!SC.kindOf(record, SC.Record)) {
330       //@if(debug)
331       SC.Logger.warn("Developer Warning: Using SC.RecordArray's `lastIndexOf` on %@, which is not an SC.Record. SC.RecordArray only works with records.".fmt(record));
332       SC.Logger.trace();
333       //@endif
334       return -1; // only takes records
335     }
336 
337     this.flush();
338 
339     var storeKey  = record.get('storeKey'),
340         storeKeys = this.get('storeKeys');
341     return storeKeys ? storeKeys.lastIndexOf(storeKey, startAt) : -1;
342   },
343 
344   /**
345     Adds the specified record to the record array if it is not already part
346     of the array.  Provided for compatibility with `SC.Set`.
347 
348     @param {SC.Record} record
349     @returns {SC.RecordArray} receiver
350   */
351   add: function(record) {
352     if (!SC.kindOf(record, SC.Record)) return this ;
353     if (this.indexOf(record)<0) this.pushObject(record);
354     return this ;
355   },
356 
357   /**
358     Removes the specified record from the array if it is not already a part
359     of the array.  Provided for compatibility with `SC.Set`.
360 
361     @param {SC.Record} record
362     @returns {SC.RecordArray} receiver
363   */
364   remove: function(record) {
365     if (!SC.kindOf(record, SC.Record)) return this ;
366     this.removeObject(record);
367     return this ;
368   },
369 
370   // ..........................................................
371   // HELPER METHODS
372   //
373 
374   /**
375     Extends the standard SC.Enumerable implementation to return results based
376     on a Query if you pass it in.
377 
378     @param {SC.Query} query a SC.Query object
379 		@param {Object} target the target object to use
380 
381     @returns {SC.RecordArray}
382   */
383   find: function(original, query, target) {
384     if (query && query.isQuery) {
385       return this.get('store').find(query.queryWithScope(this));
386     } else return original.apply(this, SC.$A(arguments).slice(1));
387   }.enhance(),
388 
389   /**
390     Call whenever you want to refresh the results of this query.  This will
391     notify the data source, asking it to refresh the contents.
392 
393     @returns {SC.RecordArray} receiver
394   */
395   refresh: function() {
396     this.get('store').refreshQuery(this.get('query'));
397     return this;
398   },
399 
400   /**
401     Will recompute the results of the attached `SC.Query`. Useful if your query
402     is based on computed properties that might have changed.
403 
404     This method is for local use only, operating only on records that have already
405     been loaded into your store. If you wish to re-fetch a remote query via your
406     data source, use `refresh()` instead.
407 
408     @returns {SC.RecordArray} receiver
409   */
410   reload: function() {
411     this.flush(YES);
412     return this;
413   },
414 
415   /**
416     Destroys the record array.  Releases any `storeKeys`, and deregisters with
417     the owner store.
418 
419     @returns {SC.RecordArray} receiver
420   */
421   destroy: function() {
422     if (!this.get('isDestroyed')) {
423       this.get('store').recordArrayWillDestroy(this);
424     }
425 
426     sc_super();
427   },
428 
429   // ..........................................................
430   // STORE CALLBACKS
431   //
432 
433   // **NOTE**: `storeWillFetchQuery()`, `storeDidFetchQuery()`,
434   // `storeDidCancelQuery()`, and `storeDidErrorQuery()` are tested implicitly
435   // through the related methods in `SC.Store`.  We're doing it this way
436   // because eventually this particular implementation is likely to change;
437   // moving some or all of this code directly into the store. -CAJ
438 
439   /** @private
440     Called whenever the store initiates a refresh of the query.  Sets the
441     status of the record array to the appropriate status.
442 
443     @param {SC.Query} query
444     @returns {SC.RecordArray} receiver
445   */
446   storeWillFetchQuery: function(query) {
447     var status = this.get('status'),
448         K      = SC.Record;
449     if ((status === K.EMPTY) || (status === K.ERROR)) status = K.BUSY_LOADING;
450     if (status & K.READY) status = K.BUSY_REFRESH;
451     this.setIfChanged('status', status);
452     return this ;
453   },
454 
455   /** @private
456     Called whenever the store has finished fetching a query.
457 
458     @param {SC.Query} query
459     @returns {SC.RecordArray} receiver
460   */
461   storeDidFetchQuery: function(query) {
462     this.setIfChanged('status', SC.Record.READY_CLEAN);
463     return this ;
464   },
465 
466   /** @private
467     Called whenever the store has cancelled a refresh.  Sets the
468     status of the record array to the appropriate status.
469 
470     @param {SC.Query} query
471     @returns {SC.RecordArray} receiver
472   */
473   storeDidCancelQuery: function(query) {
474     var status = this.get('status'),
475         K      = SC.Record;
476     if (status === K.BUSY_LOADING) status = K.EMPTY;
477     else if (status === K.BUSY_REFRESH) status = K.READY_CLEAN;
478     this.setIfChanged('status', status);
479     return this ;
480   },
481 
482   /** @private
483     Called whenever the store encounters an error while fetching.  Sets the
484     status of the record array to the appropriate status.
485 
486     @param {SC.Query} query
487     @returns {SC.RecordArray} receiver
488   */
489   storeDidErrorQuery: function(query) {
490     this.setIfChanged('status', SC.Record.ERROR);
491     return this ;
492   },
493 
494   /** @private
495     Called by the store whenever it changes the state of certain store keys. If
496     the receiver cares about these changes, it will mark itself as dirty and add
497     the changed store keys to the _scq_changedStoreKeys index set.
498 
499     The next time you try to access the record array, it will call `flush()` and
500     add the changed keys to the underlying `storeKeys` array if the new records
501     match the conditions of the record array's query.
502 
503     @param {SC.Array} storeKeys the effected store keys
504     @param {SC.Set} recordTypes the record types for the storeKeys.
505     @returns {SC.RecordArray} receiver
506   */
507   storeDidChangeStoreKeys: function(storeKeys, recordTypes) {
508     var query =  this.get('query');
509     // fast path exits
510     if (query.get('location') !== SC.Query.LOCAL) return this;
511     if (!query.containsRecordTypes(recordTypes)) return this;
512 
513     // ok - we're interested.  mark as dirty and save storeKeys.
514     var changed = this._scq_changedStoreKeys;
515     if (!changed) changed = this._scq_changedStoreKeys = SC.IndexSet.create();
516     changed.addEach(storeKeys);
517 
518     this.set('needsFlush', YES);
519     if (this.get('storeKeys')) {
520       this.flush();
521     }
522 
523     return this;
524   },
525 
526   /**
527     Applies the query to any pending changed store keys, updating the record
528     array contents as necessary.  This method is called automatically anytime
529     you access the RecordArray to make sure it is up to date, but you can
530     call it yourself as well if you need to force the record array to fully
531     update immediately.
532 
533     Currently this method only has an effect if the query location is
534     `SC.Query.LOCAL`.  You can call this method on any `RecordArray` however,
535     without an error.
536 
537     @param {Boolean} _flush to force it - use reload() to trigger it
538     @returns {SC.RecordArray} receiver
539   */
540   flush: function(_flush) {
541     // Are we already inside a flush?  If so, then don't do it again, to avoid
542     // never-ending recursive flush calls.  Instead, we'll simply mark
543     // ourselves as needing a flush again when we're done.
544     if (this._insideFlush) {
545       this.set('needsFlush', YES);
546       return this;
547     }
548 
549     if (!this.get('needsFlush') && !_flush) return this; // nothing to do
550     this.set('needsFlush', NO); // avoid running again.
551 
552     // fast exit
553     var query = this.get('query'),
554         store = this.get('store');
555     if (!store || !query || query.get('location') !== SC.Query.LOCAL) {
556       return this;
557     }
558 
559     this._insideFlush = YES;
560 
561     // OK, actually generate some results
562     var storeKeys = this.get('storeKeys'),
563         changed   = this._scq_changedStoreKeys,
564         didChange = NO,
565         K         = SC.Record,
566         storeKeysToPace = [],
567         startDate = new Date(),
568         rec, status, recordType, sourceKeys, scope, included;
569 
570     // if we have storeKeys already, just look at the changed keys
571     var oldStoreKeys = storeKeys;
572     if (storeKeys && !_flush) {
573 
574       if (changed) {
575         changed.forEach(function(storeKey) {
576           if(storeKeysToPace.length>0 || new Date()-startDate>SC.RecordArray.QUERY_MATCHING_THRESHOLD) {
577             storeKeysToPace.push(storeKey);
578             return;
579           }
580           // get record - do not include EMPTY or DESTROYED records
581           status = store.peekStatus(storeKey);
582           if (!(status & K.EMPTY) && !((status & K.DESTROYED) || (status === K.BUSY_DESTROYING))) {
583             rec = store.materializeRecord(storeKey);
584             included = !!(rec && query.contains(rec));
585           } else included = NO ;
586 
587           // if storeKey should be in set but isn't -- add it.
588           if (included) {
589             if (storeKeys.indexOf(storeKey)<0) {
590               if (!didChange) storeKeys = storeKeys.copy();
591               storeKeys.pushObject(storeKey);
592             }
593           // if storeKey should NOT be in set but IS -- remove it
594           } else {
595             if (storeKeys.indexOf(storeKey)>=0) {
596               if (!didChange) storeKeys = storeKeys.copy();
597               storeKeys.removeObject(storeKey);
598             } // if (storeKeys.indexOf)
599           } // if (included)
600 
601         }, this);
602         // make sure resort happens
603         didChange = YES ;
604 
605       } // if (changed)
606 
607     // if no storeKeys, then we have to go through all of the storeKeys
608     // and decide if they belong or not.  ick.
609     } else {
610 
611       // collect the base set of keys.  if query has a parent scope, use that
612       if (scope = query.get('scope')) {
613         sourceKeys = scope.flush().get('storeKeys');
614       // otherwise, lookup all storeKeys for the named recordType...
615       } else if (recordType = query.get('expandedRecordTypes')) {
616         sourceKeys = SC.IndexSet.create();
617         recordType.forEach(function(cur) {
618           sourceKeys.addEach(store.storeKeysFor(cur));
619         });
620       }
621 
622       // loop through storeKeys to determine if it belongs in this query or
623       // not.
624       storeKeys = [];
625       sourceKeys.forEach(function(storeKey) {
626         if(storeKeysToPace.length>0 || new Date()-startDate>SC.RecordArray.QUERY_MATCHING_THRESHOLD) {
627           storeKeysToPace.push(storeKey);
628           return;
629         }
630 
631         status = store.peekStatus(storeKey);
632         if (!(status & K.EMPTY) && !((status & K.DESTROYED) || (status === K.BUSY_DESTROYING))) {
633           rec = store.materializeRecord(storeKey);
634           if (rec && query.contains(rec)) storeKeys.push(storeKey);
635         }
636       });
637 
638       didChange = YES ;
639     }
640 
641     // if we reach our threshold of pacing we need to schedule the rest of the
642     // storeKeys to also be updated
643     if (storeKeysToPace.length > 0) {
644       this.invokeNext(function () {
645         if (!this || this.get('isDestroyed')) return;
646         this.set('needsFlush', YES);
647         this._scq_changedStoreKeys = SC.IndexSet.create().addEach(storeKeysToPace);
648         this.flush();
649       });
650     }
651 
652     // clear set of changed store keys
653     if (changed) changed.clear();
654 
655     // Clear the flushing flag.
656     // NOTE: Do this now, because any observers of storeKeys could trigger a call
657     // to flush (ex. by calling get('length') on the RecordArray).
658     this._insideFlush = NO;
659 
660     // only resort and update if we did change
661     if (didChange) {
662 
663       // storeKeys must be a new instance because orderStoreKeys() works on it
664       if (storeKeys && (storeKeys===oldStoreKeys)) {
665         storeKeys = storeKeys.copy();
666       }
667 
668       storeKeys = SC.Query.orderStoreKeys(storeKeys, query, store);
669       if (SC.compare(oldStoreKeys, storeKeys) !== 0){
670         this.set('storeKeys', SC.clone(storeKeys)); // replace content
671       }
672     }
673 
674     return this;
675   },
676 
677   /**
678     Set to `YES` when the query is dirty and needs to update its storeKeys
679     before returning any results.  `RecordArray`s always start dirty and become
680     clean the first time you try to access their contents.
681 
682     @type Boolean
683   */
684   needsFlush: YES,
685 
686   // ..........................................................
687   // EMULATE SC.ERROR API
688   //
689 
690   /**
691     Returns `YES` whenever the status is `SC.Record.ERROR`.  This will allow
692     you to put the UI into an error state.
693 
694 		@property
695     @type Boolean
696   */
697   isError: function() {
698     return !!(this.get('status') & SC.Record.ERROR);
699   }.property('status').cacheable(),
700 
701   /**
702     Returns the receiver if the record array is in an error state.  Returns
703     `null` otherwise.
704 
705 		@property
706     @type SC.Record
707   */
708   errorValue: function() {
709     return this.get('isError') ? SC.val(this.get('errorObject')) : null ;
710   }.property('isError').cacheable(),
711 
712   /**
713     Returns the current error object only if the record array is in an error
714     state. If no explicit error object has been set, returns
715     `SC.Record.GENERIC_ERROR.`
716 
717 		@property
718     @type SC.Error
719   */
720   errorObject: function() {
721     if (this.get('isError')) {
722       var store = this.get('store');
723       return store.readQueryError(this.get('query')) || SC.Record.GENERIC_ERROR;
724     } else return null ;
725   }.property('isError').cacheable(),
726 
727   // ..........................................................
728   // INTERNAL SUPPORT
729   //
730 
731   propertyWillChange: function(key) {
732     if (key === 'storeKeys') {
733       var storeKeys = this.get('storeKeys');
734       var len = storeKeys ? storeKeys.get('length') : 0;
735 
736       this.arrayContentWillChange(0, len, 0);
737     }
738 
739     return sc_super();
740   },
741 
742   /** @private
743     Invoked whenever the `storeKeys` array changes.  Observes changes.
744   */
745   _storeKeysDidChange: function() {
746     var storeKeys = this.get('storeKeys');
747 
748     var prev = this._prevStoreKeys, oldLen, newLen;
749 
750     if (storeKeys === prev) { return; } // nothing to do
751 
752     if (prev) {
753       prev.removeArrayObservers({
754         target: this,
755         willChange: this.arrayContentWillChange,
756         didChange: this._storeKeysContentDidChange
757       });
758 
759       oldLen = prev.get('length');
760     } else {
761       oldLen = 0;
762     }
763 
764     this._prevStoreKeys = storeKeys;
765     if (storeKeys) {
766       storeKeys.addArrayObservers({
767         target: this,
768         willChange: this.arrayContentWillChange,
769         didChange: this._storeKeysContentDidChange
770       });
771 
772       newLen = storeKeys.get('length');
773     } else {
774       newLen = 0;
775     }
776 
777     this._storeKeysContentDidChange(0, oldLen, newLen);
778 
779   }.observes('storeKeys'),
780 
781   /** @private
782     If anyone adds an array observer on to the record array, make sure
783     we flush so that the observers don't fire the first time length is
784     calculated.
785   */
786   addArrayObservers: function() {
787     this.flush();
788     return SC.Array.addArrayObservers.apply(this, arguments);
789   },
790 
791   /** @private
792     Invoked whenever the content of the `storeKeys` array changes.  This will
793     dump any cached record lookup and then notify that the enumerable content
794     has changed.
795   */
796   _storeKeysContentDidChange: function(start, removedCount, addedCount) {
797     if (this._scra_records) this._scra_records.length=0 ; // clear cache
798 
799     this.arrayContentDidChange(start, removedCount, addedCount);
800   },
801 
802   /** @private */
803   init: function() {
804     sc_super();
805     this._storeKeysDidChange();
806   }
807 
808 });
809 
810 SC.RecordArray.mixin(/** @scope SC.RecordArray.prototype */{
811 
812   /**
813     Standard error throw when you try to modify a record that is not editable
814 
815     @type SC.Error
816   */
817   NOT_EDITABLE: SC.$error("SC.RecordArray is not editable"),
818 
819   /**
820     Number of milliseconds to allow a query matching to run for. If this number
821     is exceeded, the query matching will be paced so as to not lock up the
822     browser (by essentially splitting the work with an invokeNext)
823 
824     @type Number
825   */
826   QUERY_MATCHING_THRESHOLD: 100
827 });
828