// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2010 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== sc_require('system/store'); /** @class A nested store can buffer changes to a parent store and then commit them all at once. You usually will use a NestedStore as part of store chaining to stage changes to your object graph before sharing them with the rest of the application. Normally you will not create a nested store directly. Instead, you can retrieve a nested store by using the chain() method. When you are finished working with the nested store, destroy() will dispose of it. @extends SC.Store @since SproutCore 1.0 */ SC.NestedStore = SC.Store.extend( /** @scope SC.NestedStore.prototype */ { /** This is set to YES when there are changes that have not been committed yet. @property {Boolean} @default NO */ hasChanges: NO, /** The parent store this nested store is chained to. Nested stores must have a parent store in order to function properly. Normally, you create a nested store using the SC.Store#chain() method and this property will be set for you. @property {SC.Store} */ parentStore: null, /** YES if the view is nested. Walk like a duck @property {Boolean} */ isNested: YES, /** If YES, then the attribute hash state will be locked when you first read the data hash or status. This means that if you retrieve a record then change the record in the parent store, the changes will not be visible to your nested store until you commit or discard changes. If NO, then the attribute hash will lock only when you write data. Normally you want to lock your attribute hash the first time you read it. This will make your nested store behave most consistently. However, if you are using multiple sibling nested stores at one time, you may want to turn off this property so that changes from one store will be reflected in the other one immediately. In this case you will be responsible for ensuring that the sibling stores do not edit the same part of the object graph at the same time. @property {Boolean} */ lockOnRead: YES, /** @private Array contains the base revision for an attribute hash when it was first cloned from the parent store. If the attribute hash is edited and commited, the commit will fail if the parent attributes hash has been edited since. This is a form of optimistic locking, hence the name. Each store gets its own array of locks, which are selectively populated as needed. Note that this is kept as an array because it will be stored as a dense array on some browsers, making it faster. @property {Array} */ locks: null, /** @private An array that includes the store keys that have changed since the store was last committed. This array is used to sync data hash changes between chained stores. For a log changes that may actually be committed back to the server see the changelog property. @property {SC.Set} */ chainedChanges: null, // .......................................................... // STORE CHAINING // /** find() cannot accept REMOTE queries in a nested store. This override will verify that condition for you. See SC.Store#find() for info on using this method. @returns {SC.Record|SC.RecordArray} */ find: function(query) { if (query && query.isQuery && query.get('location') !== SC.Query.LOCAL) { throw "SC.Store#find() can only accept LOCAL queries in nested stores"; } return sc_super(); }, /** Propagate this store's changes to its parent. If the store does not have a parent, this has no effect other than to clear the change set. @param {Boolean} force if YES, does not check for conflicts first @returns {SC.Store} receiver */ commitChanges: function(force) { if (this.get('hasChanges')) { var pstore = this.get('parentStore'); pstore.commitChangesFromNestedStore(this, this.chainedChanges, force); } // clear out custom changes - even if there is nothing to commit. this.reset(); return this ; }, /** Discard the changes made to this store and reset the store. @returns {SC.Store} receiver */ discardChanges: function() { // any locked records whose rev or lock rev differs from parent need to // be notified. var records, locks; if ((records = this.records) && (locks = this.locks)) { var pstore = this.get('parentStore'), psRevisions = pstore.revisions; var revisions = this.revisions, storeKey, lock, rev; for (storeKey in records) { if (!records.hasOwnProperty(storeKey)) continue ; if (!(lock = locks[storeKey])) continue; // not locked. rev = psRevisions[storeKey]; if ((rev !== lock) || (revisions[storeKey] > rev)) { this._notifyRecordPropertyChange(parseInt(storeKey, 10)); } } } this.reset(); this.flush(); return this ; }, /** When you are finished working with a chained store, call this method to tear it down. This will also discard any pending changes. @returns {SC.Store} receiver */ destroy: function() { this.discardChanges(); var parentStore = this.get('parentStore'); if (parentStore) parentStore.willDestroyNestedStore(this); sc_super(); return this ; }, /** Resets a store's data hash contents to match its parent. @returns {SC.Store} receiver */ reset: function() { var nRecords, nr, sk; // requires a pstore to reset var parentStore = this.get('parentStore'); if (!parentStore) throw SC.Store.NO_PARENT_STORE_ERROR; // inherit data store from parent store. this.dataHashes = SC.beget(parentStore.dataHashes); this.revisions = SC.beget(parentStore.revisions); this.statuses = SC.beget(parentStore.statuses); // beget nested records references this.childRecords = parentStore.childRecords ? SC.beget(parentStore.childRecords) : {}; this.parentRecords = parentStore.parentRecords ? SC.beget(parentStore.parentRecords) : {}; // also, reset private temporary objects this.chainedChanges = this.locks = this.editables = null; this.changelog = null ; // TODO: Notify record instances this.set('hasChanges', NO); }, /** @private Chain to parentstore */ refreshQuery: function(query) { var parentStore = this.get('parentStore'); if (parentStore) parentStore.refreshQuery(query); return this ; }, /** Returns the SC.Error object associated with a specific record. Delegates the call to the parent store. @param {Number} storeKey The store key of the record. @returns {SC.Error} SC.Error or null if no error associated with the record. */ readError: function(storeKey) { var parentStore = this.get('parentStore'); return parentStore ? parentStore.readError(storeKey) : null; }, /** Returns the SC.Error object associated with a specific query. Delegates the call to the parent store. @param {SC.Query} query The SC.Query with which the error is associated. @returns {SC.Error} SC.Error or null if no error associated with the query. */ readQueryError: function(query) { var parentStore = this.get('parentStore'); return parentStore ? parentStore.readQueryError(query) : null; }, // .......................................................... // CORE ATTRIBUTE API // // The methods in this layer work on data hashes in the store. They do not // perform any changes that can impact records. Usually you will not need // to use these methods. /** Returns the current edit status of a storekey. May be one of INHERITED, EDITABLE, and LOCKED. Used mostly for unit testing. @param {Number} storeKey the store key @returns {Number} edit status */ storeKeyEditState: function(storeKey) { var editables = this.editables, locks = this.locks; return (editables && editables[storeKey]) ? SC.Store.EDITABLE : (locks && locks[storeKey]) ? SC.Store.LOCKED : SC.Store.INHERITED ; }, /** @private Locks the data hash so that it iterates independently from the parent store. */ _lock: function(storeKey) { var locks = this.locks, rev, editables, pk, pr, path, tup, obj, key; // already locked -- nothing to do if (locks && locks[storeKey]) return this; // create locks if needed if (!locks) locks = this.locks = []; // fixup editables editables = this.editables; if (editables) editables[storeKey] = 0; // if the data hash in the parent store is editable, then clone the hash // for our own use. Otherwise, just copy a reference to the data hash // in the parent store. -- find first non-inherited state var pstore = this.get('parentStore'), editState; while(pstore && (editState=pstore.storeKeyEditState(storeKey)) === SC.Store.INHERITED) { pstore = pstore.get('parentStore'); } if (pstore && editState === SC.Store.EDITABLE) { pk = this.childRecords[storeKey]; if (pk){ // Since this is a nested record we have to actually walk up the parent chain // to get to the root parent and clone that hash. And then reconstruct the // memory space linking. this._lock(pk); pr = this.parentRecords[pk]; if (pr) { path = pr[storeKey]; tup = path ? SC.tupleForPropertyPath(path, this.dataHashes[pk]) : null; if (tup){ obj = tup[0]; key = tup[1]; } this.dataHashes[storeKey] = obj && key ? obj[key] : null; } } else { this.dataHashes[storeKey] = SC.clone(pstore.dataHashes[storeKey], YES); } if (!editables) editables = this.editables = []; editables[storeKey] = 1 ; // mark as editable } else this.dataHashes[storeKey] = this.dataHashes[storeKey]; // also copy the status + revision this.statuses[storeKey] = this.statuses[storeKey]; rev = this.revisions[storeKey] = this.revisions[storeKey]; // save a lock and make it not editable locks[storeKey] = rev || 1; return this ; }, /** @private - adds chaining support */ readDataHash: function(storeKey) { if (this.get('lockOnRead')) this._lock(storeKey); return this.dataHashes[storeKey]; }, /** @private - adds chaining support */ readEditableDataHash: function(storeKey) { // lock the data hash if needed this._lock(storeKey); return sc_super(); }, /** @private - adds chaining support - Does not call sc_super because the implementation of the method vary too much. */ writeDataHash: function(storeKey, hash, status) { var locks = this.locks, didLock = NO, rev ; // Update our dataHash and/or status, depending on what was passed in. // Note that if no new hash was passed in, we'll lock the storeKey to // properly fork our dataHash from our parent store. Similarly, if no // status was passed in, we'll save our own copy of the value. if (hash) { this.dataHashes[storeKey] = hash; } else { this._lock(storeKey); didLock = YES; } if (status) { this.statuses[storeKey] = status; } else { if (!didLock) this.statuses[storeKey] = (this.statuses[storeKey] || SC.Record.READY_NEW); } if (!didLock) { rev = this.revisions[storeKey] = this.revisions[storeKey]; // copy ref // make sure we lock if needed. if (!locks) locks = this.locks = []; if (!locks[storeKey]) locks[storeKey] = rev || 1; } // Also note that this hash is now editable. (Even if we locked it, // above, it may not have been marked as editable.) var editables = this.editables; if (!editables) editables = this.editables = []; editables[storeKey] = 1 ; // use number for dense array support return this ; }, /** @private - adds chaining support */ removeDataHash: function(storeKey, status) { // record optimistic lock revision var locks = this.locks; if (!locks) locks = this.locks = []; if (!locks[storeKey]) locks[storeKey] = this.revisions[storeKey] || 1; return sc_super(); }, /** @private - book-keeping for a single data hash. */ dataHashDidChange: function(storeKeys, rev, statusOnly, key) { // update the revision for storeKey. Use generateStoreKey() because that // gaurantees a universally (to this store hierarchy anyway) unique // key value. if (!rev) rev = SC.Store.generateStoreKey(); var isArray, len, idx, storeKey; isArray = SC.typeOf(storeKeys) === SC.T_ARRAY; if (isArray) { len = storeKeys.length; } else { len = 1; storeKey = storeKeys; } var changes = this.chainedChanges; if (!changes) changes = this.chainedChanges = SC.Set.create(); for(idx=0;idx0); this.flush(); return this ; }, // .......................................................... // HIGH-LEVEL RECORD API // /** @private - adapt for nested store */ queryFor: function(recordType, conditions, params) { return this.get('parentStore').queryFor(recordType, conditions, params); }, /** @private - adapt for nested store */ findAll: function(recordType, conditions, params, recordArray, _store) { if (!_store) _store = this; return this.get('parentStore').findAll(recordType, conditions, params, recordArray, _store); }, // .......................................................... // CORE RECORDS API // // The methods in this section can be used to manipulate records without // actually creating record instances. /** @private - adapt for nested store Unlike for the main store, for nested stores if isRefresh=YES, we'll throw an error if the record is dirty. We'll otherwise avoid setting our status because that can disconnect us from upper and/or lower stores. */ retrieveRecords: function(recordTypes, ids, storeKeys, isRefresh) { var pstore = this.get('parentStore'), idx, storeKey, newStatus, len = (!storeKeys) ? ids.length : storeKeys.length, K = SC.Record, status; // Is this a refresh? if (isRefresh) { for(idx=0;idx