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