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('system/store'); 9 10 /** 11 @class 12 13 A nested store can buffer changes to a parent store and then commit them 14 all at once. You usually will use a `NestedStore` as part of store chaining 15 to stage changes to your object graph before sharing them with the rest of 16 the application. 17 18 Normally you will not create a nested store directly. Instead, you can 19 retrieve a nested store by using the `chain()` method. When you are finished 20 working with the nested store, `destroy()` will dispose of it. 21 22 @extends SC.Store 23 @since SproutCore 1.0 24 */ 25 SC.NestedStore = SC.Store.extend( 26 /** @scope SC.NestedStore.prototype */ { 27 28 /** 29 This is set to YES when there are changes that have not been committed 30 yet. 31 32 @type Boolean 33 @default NO 34 */ 35 hasChanges: NO, 36 37 /** 38 The parent store this nested store is chained to. Nested stores must have 39 a parent store in order to function properly. Normally, you create a 40 nested store using the `SC.Store#chain()` method and this property will be 41 set for you. 42 43 @type SC.Store 44 @default null 45 */ 46 parentStore: null, 47 48 /** 49 `YES` if the store is nested. Walk like a duck 50 51 @type Boolean 52 @default YES 53 */ 54 isNested: YES, 55 56 /** 57 If YES, then the attribute hash state will be locked when you first 58 read the data hash or status. This means that if you retrieve a record 59 then change the record in the parent store, the changes will not be 60 visible to your nested store until you commit or discard changes. 61 62 If `NO`, then the attribute hash will lock only when you write data. 63 64 Normally you want to lock your attribute hash the first time you read it. 65 This will make your nested store behave most consistently. However, if 66 you are using multiple sibling nested stores at one time, you may want 67 to turn off this property so that changes from one store will be reflected 68 in the other one immediately. In this case you will be responsible for 69 ensuring that the sibling stores do not edit the same part of the object 70 graph at the same time. 71 72 @type Boolean 73 @default YES 74 */ 75 lockOnRead: YES, 76 77 /** @private 78 Array contains the base revision for an attribute hash when it was first 79 cloned from the parent store. If the attribute hash is edited and 80 committed, the commit will fail if the parent attributes hash has been 81 edited since. 82 83 This is a form of optimistic locking, hence the name. 84 85 Each store gets its own array of locks, which are selectively populated 86 as needed. 87 88 Note that this is kept as an array because it will be stored as a dense 89 array on some browsers, making it faster. 90 91 @type Array 92 @default null 93 */ 94 locks: null, 95 96 /** @private 97 An array that includes the store keys that have changed since the store 98 was last committed. This array is used to sync data hash changes between 99 chained stores. For a log changes that may actually be committed back to 100 the server see the changelog property. 101 102 @type SC.Set 103 @default YES 104 */ 105 chainedChanges: null, 106 107 // .......................................................... 108 // STORE CHAINING 109 // 110 111 /** 112 `find()` cannot accept REMOTE queries in a nested store. This override will 113 verify that condition for you. See `SC.Store#find()` for info on using this 114 method. 115 116 @param {SC.Query} query query object to use. 117 @returns {SC.Record|SC.RecordArray} 118 */ 119 find: function(query) { 120 if (query && query.isQuery && query.get('location') !== SC.Query.LOCAL) { 121 throw new Error("SC.Store#find() can only accept LOCAL queries in nested stores"); 122 } 123 return sc_super(); 124 }, 125 126 /** 127 Propagate this store's changes to its parent. If the store does not 128 have a parent, this has no effect other than to clear the change set. 129 130 @param {Boolean} force if YES, does not check for conflicts first 131 @returns {SC.Store} receiver 132 */ 133 commitChanges: function(force) { 134 if (this.get('hasChanges')) { 135 var pstore = this.get('parentStore'); 136 pstore.commitChangesFromNestedStore(this, this.get('chainedChanges'), force); 137 } 138 139 // clear out custom changes - even if there is nothing to commit. 140 this.reset(); 141 return this; 142 }, 143 144 /** 145 An array of store keys for all conflicting records with the parent store. If there are no 146 conflicting records, this property will be null. 147 148 @readonly 149 @field 150 @type Array 151 @default null 152 */ 153 conflictedStoreKeys: function () { 154 var ret = null; 155 156 if (this.get('hasChanges')) { 157 var pstore = this.get('parentStore'), 158 locks = this.locks, 159 revisions = pstore.revisions; 160 161 if (locks && revisions) { 162 var changes = this.get('chainedChanges'); 163 164 for (var i = 0, len = changes.length; i < len; i++) { 165 var storeKey = changes[i], 166 lock = locks[storeKey] || 1, 167 revision = revisions[storeKey] || 1; 168 169 // If the same revision for the item does not match the current revision, then someone has 170 // changed the data hash in this store and we have a conflict. 171 if (lock < revision) { 172 if (!ret) ret = []; 173 ret.push(storeKey); 174 } 175 } 176 } 177 } 178 179 return ret; 180 }.property('chainedChanges').cacheable(), 181 182 /** 183 Propagate this store's successful changes to its parent (if exists). At the end, it clears the 184 local, private status of the committed records therefore the method can be called several times 185 until the full transaction is successful or editing is abandoned 186 187 @param {Boolean} force if YES, does not check for conflicts first 188 @returns {SC.Store} receiver 189 */ 190 commitSuccessfulChanges: function(force) { 191 var chainedChanges = this.get('chainedChanges'); 192 193 if (this.get('hasChanges') && chainedChanges) { 194 var dataHashes = this.dataHashes, 195 revisions = this.revisions, 196 statuses = this.statuses, 197 editables = this.editables, 198 locks = this.locks; 199 200 var successfulChanges = chainedChanges.filter( function(storeKey) { 201 var state = this.readStatus(storeKey); 202 203 return state === SC.Record.READY_CLEAN || state === SC.Record.DESTROYED_CLEAN; 204 }, this ); 205 206 var pstore = this.get('parentStore'); 207 208 pstore.commitChangesFromNestedStore(this, successfulChanges, force); 209 210 // remove the local status so these records that have been successfully committed on the server 211 // are no longer retrieved from this nested store but from the parent 212 for (var i = 0, len = successfulChanges.get('length'); i < len; i++) { 213 var storeKey = successfulChanges.objectAt(i); 214 215 if (dataHashes && dataHashes.hasOwnProperty(storeKey)) { delete dataHashes[storeKey]; } 216 if (revisions && revisions.hasOwnProperty(storeKey)) { delete revisions[storeKey]; } 217 if (editables) { delete editables[storeKey]; } 218 if (locks) { delete locks[storeKey]; } 219 if (statuses && statuses.hasOwnProperty(storeKey)) { delete statuses[storeKey]; } 220 221 chainedChanges.remove(storeKey); 222 } 223 224 // Indicate that chainedChanges has changed. 225 if (successfulChanges.length > 0) { this.notifyPropertyChange('chainedChanges'); } 226 } 227 228 return this; 229 }, 230 231 /** 232 Discard the changes made to this store and reset the store. 233 234 @returns {SC.Store} receiver 235 */ 236 discardChanges: function() { 237 // any locked records whose rev or lock rev differs from parent need to 238 // be notified. 239 var records, locks; 240 if ((records = this.records) && (locks = this.locks)) { 241 var pstore = this.get('parentStore'), psRevisions = pstore.revisions; 242 var revisions = this.revisions, storeKey, lock, rev; 243 for (storeKey in records) { 244 if (!records.hasOwnProperty(storeKey)) continue ; 245 if (!(lock = locks[storeKey])) continue; // not locked. 246 247 rev = psRevisions[storeKey]; 248 if ((rev !== lock) || (revisions[storeKey] > rev)) { 249 this._notifyRecordPropertyChange(parseInt(storeKey, 10)); 250 } 251 } 252 } 253 254 this.reset(); 255 this.flush(); 256 return this ; 257 }, 258 259 /** 260 When you are finished working with a chained store, call this method to 261 tear it down. This will also discard any pending changes. 262 263 @returns {SC.Store} receiver 264 */ 265 destroy: function() { 266 this.discardChanges(); 267 268 var parentStore = this.get('parentStore'); 269 if (parentStore) parentStore.willDestroyNestedStore(this); 270 271 sc_super(); 272 return this ; 273 }, 274 275 /** 276 Resets a store's data hash contents to match its parent. 277 */ 278 reset: function() { 279 // requires a pstore to reset 280 var parentStore = this.get('parentStore'); 281 if (!parentStore) SC.Store.NO_PARENT_STORE_ERROR.throw(); 282 283 // inherit data store from parent store. 284 this.dataHashes = SC.beget(parentStore.dataHashes); 285 this.revisions = SC.beget(parentStore.revisions); 286 this.statuses = SC.beget(parentStore.statuses); 287 288 // beget nested records references 289 this.childRecords = parentStore.childRecords ? SC.beget(parentStore.childRecords) : {}; 290 this.parentRecords = parentStore.parentRecords ? SC.beget(parentStore.parentRecords) : {}; 291 292 // also, reset private temporary objects 293 this.set('hasChanges', false); 294 this.chainedChanges = this.locks = this.editables = null; 295 this.changelog = null ; 296 297 // TODO: Notify record instances 298 }, 299 300 /** @private 301 302 Chain to parentstore 303 */ 304 refreshQuery: function(query) { 305 var parentStore = this.get('parentStore'); 306 if (parentStore) parentStore.refreshQuery(query); 307 return this ; 308 }, 309 310 /** 311 Returns the `SC.Error` object associated with a specific record. 312 313 Delegates the call to the parent store. 314 315 @param {Number} storeKey The store key of the record. 316 317 @returns {SC.Error} SC.Error or null if no error associated with the record. 318 */ 319 readError: function(storeKey) { 320 var parentStore = this.get('parentStore'); 321 return parentStore ? parentStore.readError(storeKey) : null; 322 }, 323 324 /** 325 Returns the `SC.Error` object associated with a specific query. 326 327 Delegates the call to the parent store. 328 329 @param {SC.Query} query The SC.Query with which the error is associated. 330 331 @returns {SC.Error} SC.Error or null if no error associated with the query. 332 */ 333 readQueryError: function(query) { 334 var parentStore = this.get('parentStore'); 335 return parentStore ? parentStore.readQueryError(query) : null; 336 }, 337 338 /** @private - adapt for nested store */ 339 chainAutonomousStore: function(attrs, newStoreClass) { 340 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 341 }, 342 343 // .......................................................... 344 // CORE ATTRIBUTE API 345 // 346 // The methods in this layer work on data hashes in the store. They do not 347 // perform any changes that can impact records. Usually you will not need 348 // to use these methods. 349 350 /** 351 Returns the current edit status of a storekey. May be one of `INHERITED`, 352 `EDITABLE`, and `LOCKED`. Used mostly for unit testing. 353 354 @param {Number} storeKey the store key 355 @returns {Number} edit status 356 */ 357 storeKeyEditState: function(storeKey) { 358 var editables = this.editables, locks = this.locks; 359 return (editables && editables[storeKey]) ? SC.Store.EDITABLE : (locks && locks[storeKey]) ? SC.Store.LOCKED : SC.Store.INHERITED ; 360 }, 361 362 /** @private 363 Locks the data hash so that it iterates independently from the parent 364 store. 365 */ 366 _lock: function(storeKey) { 367 var locks = this.locks, rev, editables, 368 pk, pr, path, tup, obj, key; 369 370 // already locked -- nothing to do 371 if (locks && locks[storeKey]) return this; 372 373 // create locks if needed 374 if (!locks) locks = this.locks = []; 375 376 // fixup editables 377 editables = this.editables; 378 if (editables) editables[storeKey] = 0; 379 380 381 // if the data hash in the parent store is editable, then clone the hash 382 // for our own use. Otherwise, just copy a reference to the data hash 383 // in the parent store. -- find first non-inherited state 384 var pstore = this.get('parentStore'), editState; 385 while(pstore && (editState=pstore.storeKeyEditState(storeKey)) === SC.Store.INHERITED) { 386 pstore = pstore.get('parentStore'); 387 } 388 389 if (pstore && editState === SC.Store.EDITABLE) { 390 391 pk = this.childRecords[storeKey]; 392 if (pk){ 393 // Since this is a nested record we have to actually walk up the parent chain 394 // to get to the root parent and clone that hash. And then reconstruct the 395 // memory space linking. 396 this._lock(pk); 397 pr = this.parentRecords[pk]; 398 if (pr) { 399 path = pr[storeKey]; 400 tup = path ? SC.tupleForPropertyPath(path, this.dataHashes[pk]) : null; 401 if (tup){ obj = tup[0]; key = tup[1]; } 402 this.dataHashes[storeKey] = obj && key ? obj[key] : null; 403 } 404 } 405 else { 406 this.dataHashes[storeKey] = SC.clone(pstore.dataHashes[storeKey], YES); 407 } 408 if (!editables) editables = this.editables = []; 409 editables[storeKey] = 1 ; // mark as editable 410 411 } else this.dataHashes[storeKey] = pstore.dataHashes[storeKey]; 412 413 // also copy the status + revision 414 this.statuses[storeKey] = this.statuses[storeKey]; 415 rev = this.revisions[storeKey] = this.revisions[storeKey]; 416 417 // save a lock and make it not editable 418 locks[storeKey] = rev || 1; 419 420 return this ; 421 }, 422 423 /** @private - adds chaining support */ 424 readDataHash: function(storeKey) { 425 if (this.get('lockOnRead')) this._lock(storeKey); 426 return this.dataHashes[storeKey]; 427 }, 428 429 /** @private - adds chaining support */ 430 readEditableDataHash: function(storeKey) { 431 432 // lock the data hash if needed 433 this._lock(storeKey); 434 435 return sc_super(); 436 }, 437 438 /** @private - adds chaining support - 439 Does not call sc_super because the implementation of the method vary too 440 much. 441 */ 442 writeDataHash: function(storeKey, hash, status) { 443 var locks = this.locks, didLock = NO, rev ; 444 445 // Update our dataHash and/or status, depending on what was passed in. 446 // Note that if no new hash was passed in, we'll lock the storeKey to 447 // properly fork our dataHash from our parent store. Similarly, if no 448 // status was passed in, we'll save our own copy of the value. 449 if (hash) { 450 this.dataHashes[storeKey] = hash; 451 } 452 else { 453 this._lock(storeKey); 454 didLock = YES; 455 } 456 457 if (status) { 458 this.statuses[storeKey] = status; 459 } 460 else { 461 if (!didLock) this.statuses[storeKey] = (this.statuses[storeKey] || SC.Record.READY_NEW); 462 } 463 464 if (!didLock) { 465 rev = this.revisions[storeKey] = this.revisions[storeKey]; // copy ref 466 467 // make sure we lock if needed. 468 if (!locks) locks = this.locks = []; 469 if (!locks[storeKey]) locks[storeKey] = rev || 1; 470 } 471 472 // Also note that this hash is now editable. (Even if we locked it, 473 // above, it may not have been marked as editable.) 474 var editables = this.editables; 475 if (!editables) editables = this.editables = []; 476 editables[storeKey] = 1 ; // use number for dense array support 477 478 // propagate the data to the child records 479 this._updateChildRecordHashes(storeKey, hash, status); 480 481 return this ; 482 }, 483 484 /** @private - adds chaining support */ 485 removeDataHash: function(storeKey, status) { 486 487 // record optimistic lock revision 488 var locks = this.locks; 489 if (!locks) locks = this.locks = []; 490 if (!locks[storeKey]) locks[storeKey] = this.revisions[storeKey] || 1; 491 492 return sc_super(); 493 }, 494 495 /** @private - bookkeeping for a single data hash. */ 496 dataHashDidChange: function(storeKeys, rev, statusOnly, key) { 497 // update the revision for storeKey. Use generateStoreKey() because that 498 // guarantees a universally (to this store hierarchy anyway) unique 499 // key value. 500 if (!rev) rev = SC.Store.generateStoreKey(); 501 var isArray, len, idx, storeKey; 502 503 isArray = SC.typeOf(storeKeys) === SC.T_ARRAY; 504 if (isArray) { 505 len = storeKeys.length; 506 } else { 507 len = 1; 508 storeKey = storeKeys; 509 } 510 511 var changes = this.get('chainedChanges'); 512 if (!changes) changes = this.chainedChanges = SC.Set.create(); 513 514 var that = this, 515 didAddChainedChanges = false; 516 517 for (idx = 0; idx < len; idx++) { 518 if (isArray) storeKey = storeKeys[idx]; 519 520 this._lock(storeKey); 521 this.revisions[storeKey] = rev; 522 523 if (!changes.contains(storeKey)) { 524 changes.add(storeKey); 525 didAddChainedChanges = true; 526 } 527 528 this._notifyRecordPropertyChange(storeKey, statusOnly, key); 529 530 // notify also the child records 531 this._propagateToChildren(storeKey, function(storeKey){ 532 that.dataHashDidChange(storeKey, null, statusOnly, key); 533 }); 534 } 535 536 this.setIfChanged('hasChanges', YES); 537 if (didAddChainedChanges) { 538 this.notifyPropertyChange('chainedChanges'); 539 } 540 541 return this ; 542 }, 543 544 // .......................................................... 545 // SYNCING CHANGES 546 // 547 548 /** @private - adapt for nested store */ 549 commitChangesFromNestedStore: function(nestedStore, changes, force) { 550 551 sc_super(); 552 553 // save a lock for each store key if it does not have one already 554 // also add each storeKey to my own changes set. 555 var pstore = this.get('parentStore'), psRevisions = pstore.revisions, i; 556 var myLocks = this.locks, 557 myChanges = this.get('chainedChanges'), 558 len, 559 storeKey; 560 561 if (!myLocks) myLocks = this.locks = []; 562 if (!myChanges) myChanges = this.chainedChanges = SC.Set.create(); 563 564 len = changes.length ; 565 for(i=0;i<len;i++) { 566 storeKey = changes[i]; 567 if (!myLocks[storeKey]) myLocks[storeKey] = psRevisions[storeKey]||1; 568 myChanges.add(storeKey); 569 } 570 571 // Finally, mark store as dirty if we have changes 572 this.setIfChanged('hasChanges', myChanges.get('length') > 0); 573 this.notifyPropertyChange('chainedChanges'); 574 575 this.flush(); 576 577 return this ; 578 }, 579 580 // .......................................................... 581 // HIGH-LEVEL RECORD API 582 // 583 584 585 /** @private - adapt for nested store */ 586 queryFor: function(recordType, conditions, params) { 587 return this.get('parentStore').queryFor(recordType, conditions, params); 588 }, 589 590 // .......................................................... 591 // CORE RECORDS API 592 // 593 // The methods in this section can be used to manipulate records without 594 // actually creating record instances. 595 596 /** @private - adapt for nested store 597 598 Unlike for the main store, for nested stores if isRefresh=YES, we'll throw 599 an error if the record is dirty. We'll otherwise avoid setting our status 600 because that can disconnect us from upper and/or lower stores. 601 */ 602 retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh, callbacks) { 603 var pstore = this.get('parentStore'), idx, storeKey, 604 len = (!storeKeys) ? ids.length : storeKeys.length, 605 K = SC.Record, status; 606 607 // Is this a refresh? 608 if (isRefresh) { 609 for(idx=0;idx<len;idx++) { 610 storeKey = !storeKeys ? pstore.storeKeyFor(recordTypes, ids[idx]) : storeKeys[idx]; 611 status = this.peekStatus(storeKey); 612 613 // We won't allow calling retrieve on a dirty record in a nested store 614 // (although we do allow it in the main store). This is because doing 615 // so would involve writing a unique status, and that would break the 616 // status hierarchy, so even though lower stores would complete the 617 // retrieval, the upper layers would never inherit the new statuses. 618 if (status & K.DIRTY) { 619 SC.Store.NESTED_STORE_RETRIEVE_DIRTY_ERROR.throw(); 620 } 621 else { 622 // Not dirty? Then abandon any status we had set (to re-establish 623 // any prototype linkage breakage) before asking our parent store to 624 // perform the retrieve. 625 var dataHashes = this.dataHashes, 626 revisions = this.revisions, 627 statuses = this.statuses, 628 editables = this.editables, 629 locks = this.locks; 630 631 var changed = NO; 632 var statusOnly = NO; 633 634 if (dataHashes && dataHashes.hasOwnProperty(storeKey)) { 635 delete dataHashes[storeKey]; 636 changed = YES; 637 } 638 if (revisions && revisions.hasOwnProperty(storeKey)) { 639 delete revisions[storeKey]; 640 changed = YES; 641 } 642 if (editables) delete editables[storeKey]; 643 if (locks) delete locks[storeKey]; 644 645 if (statuses && statuses.hasOwnProperty(storeKey)) { 646 delete statuses[storeKey]; 647 if (!changed) statusOnly = YES; 648 changed = YES; 649 } 650 651 if (changed) this._notifyRecordPropertyChange(storeKey, statusOnly); 652 } 653 } 654 } 655 656 return pstore.retrieveRecords(recordTypes, ids, storeKeys, isRefresh, callbacks); 657 }, 658 659 /** @private - adapt for nested store */ 660 commitRecords: function(recordTypes, ids, storeKeys) { 661 if( this.get( "dataSource" ) ) 662 return sc_super(); 663 else 664 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 665 }, 666 667 /** @private - adapt for nested store */ 668 commitRecord: function(recordType, id, storeKey) { 669 if( this.get( "dataSource" ) ) 670 return sc_super(); 671 else 672 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 673 }, 674 675 /** @private - adapt for nested store */ 676 cancelRecords: function(recordTypes, ids, storeKeys) { 677 if( this.get( "dataSource" ) ) 678 return sc_super(); 679 else 680 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 681 }, 682 683 /** @private - adapt for nested store */ 684 cancelRecord: function(recordType, id, storeKey) { 685 if( this.get( "dataSource" ) ) 686 return sc_super(); 687 else 688 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 689 }, 690 691 // .......................................................... 692 // DATA SOURCE CALLBACKS 693 // 694 // Methods called by the data source on the store 695 696 /** @private - adapt for nested store */ 697 dataSourceDidCancel: function(storeKey) { 698 if( this.get( "dataSource" ) ) 699 return sc_super(); 700 else 701 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 702 }, 703 704 /** @private - adapt for nested store */ 705 dataSourceDidComplete: function(storeKey, dataHash, newId) { 706 if( this.get( "dataSource" ) ) 707 return sc_super(); 708 else 709 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 710 }, 711 712 /** @private - adapt for nested store */ 713 dataSourceDidDestroy: function(storeKey) { 714 if( this.get( "dataSource" ) ) 715 return sc_super(); 716 else 717 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 718 }, 719 720 /** @private - adapt for nested store */ 721 dataSourceDidError: function(storeKey, error) { 722 if( this.get( "dataSource" ) ) 723 return sc_super(); 724 else 725 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 726 }, 727 728 // .......................................................... 729 // PUSH CHANGES FROM DATA SOURCE 730 // 731 732 /** @private - adapt for nested store */ 733 pushRetrieve: function(recordType, id, dataHash, storeKey) { 734 if( this.get( "dataSource" ) ) 735 return sc_super(); 736 else 737 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 738 }, 739 740 /** @private - adapt for nested store */ 741 pushDestroy: function(recordType, id, storeKey) { 742 if( this.get( "dataSource" ) ) 743 return sc_super(); 744 else 745 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 746 }, 747 748 /** @private - adapt for nested store */ 749 pushError: function(recordType, id, error, storeKey) { 750 if( this.get( "dataSource" ) ) 751 return sc_super(); 752 else 753 SC.Store.NESTED_STORE_UNSUPPORTED_ERROR.throw(); 754 } 755 756 }) ; 757 758