Skip to content

Instantly share code, notes, and snippets.

@etgryphon
Created June 11, 2009 21:36
Show Gist options
  • Save etgryphon/128251 to your computer and use it in GitHub Desktop.
Save etgryphon/128251 to your computer and use it in GitHub Desktop.
// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
// Portions ©2008-2009 Apple, Inc. All rights reserved.
// License: Licened under MIT license (see license.js)
// ==========================================================================
/**
@class
The Store is where you can find all of your records. You should also
use this to define your various types of records, since this will be
used to automatically update from data coming from the server.
You should create a store for each application. This allows the records
for apps to be kept separate, even if they live in the same page.
@extends SC.Object
@static
@since SproutCore 1.0
*/
SC.Store = SC.Object.create(
/** @scope SC.Store.prototype */ {
/**
Pushes updated data to all the named records.
This method is often called from a server to update the store with the
included record objects.
You can use this method yourself to mass update the store whenever you
retrieve new records from the server. The first parameter should contain
an array of JSON-compatible hashes. The hashes can have any properties
you want but they should at least contain the following two keys:
- guid: This is a unique identifier for the record.
- type: The name of the record type. I.e. "Contact" or "Photo"
@param dataHashes {Array} array of hash records. See discussion.
@param dataSource {Object} the data source. Usually a server object.
@param recordType {SC.Record} optional record type, used if type is not
found in the data hashes itself.
@param isLoaded {Boolean} YES if the data hashes represent the full set of
data loaded from the server. NO otherwise.
@returns {Array} Array of records that were actually created/updated.
*/
updateRecords: function(dataHashes, dataSource, recordType, isLoaded) {
this.set('updateRecordsInProgress',true) ;
var store = this ;
var ret = [] ;
if (!recordType) recordType = SC.Record ;
this.beginPropertyChanges() ;
//SC.Benchmark._bench(function() {
dataHashes.forEach(function(data) {
var rt = data.recordType || recordType;
if (data.recordType !== undefined) delete data.recordType;
var pkValue = data[rt.primaryKey()] ;
var rec = store.getRecordFor(pkValue,rt,true) ;
rec.dataSource = dataSource ;
if (data.refreshURL) rec.refreshURL = data.refreshURL;
if (data.updateURL) rec.updateURL = data.updateURL;
if (data.destroyURL) rec.destroyURL = data.destroyURL;
rec.updateAttributes(data, isLoaded, isLoaded) ;
if (rec.needsAddToStore) store.addRecord(rec) ;
if (rec.changeCount === 0) store.cleanRecord(rec);
ret.push(rec) ;
});
this.endPropertyChanges() ;
this.set('updateRecordsInProgress',false) ;
return ret ;
},
// ....................................
// Record dataSource methods
//
refreshRecords: function(records) {},
createRecords: function(recs) {
recs.invoke('set','newRecord','false') ;
this.commitRecords(recs);
},
commitRecords: function(recs) {
recs.invoke('set','isDirty','false');
},
destroyRecords: function(recs) {
var store = this ;
recs.forEach(function(r) {
r.set('isDeleted',true); store.removeRecord(r);
});
},
// ....................................
// Record Helpers
//
// Boolean Flag to tell whether the store is dirty
hasChanged: NO,
/**
Add a record instance to the store. The record will now be monitored for
changes.
*/
addRecord: function(rec) {
// save record in a cache
rec.needsAddToStore = false;
var guid = rec._storeKey();
var records = this._records[guid] || [];
records.push(rec);
this._records[guid] = records;
// global record cache
if (!this._quickCache) this._quickCache = {};
// records are cached by Class type
var records = this._quickCache[guid] || {};
var pkey = rec.get(rec.primaryKey);
records[pkey] = rec;
this._quickCache[guid] = records;
// and start observing it.
rec.addObserver('*',this, this.recordDidChange) ;
this.recordDidChange(rec) ;
},
/**
remove a record instance from the store. The record will no longer be
monitored for changes and may be deleted.
*/
removeRecord: function(rec) {
// remove from cache
var guid = rec._storeKey();
var records = this._records[guid] || [];
records = records.without(rec);
this._records[guid] = records;
// remove from quick cache
if (this._quickCache)
{
var records = this._quickCache[guid] || {};
var pkey = rec.get(rec.primaryKey);
delete records[pkey];
this._quickCache[guid] = records;
}
// and stop observing it.
rec.removeObserver('*',this, this.recordDidChange) ;
this.recordDidChange(rec) ; // this will remove from cols since destroyed.
},
cleanRecord: function(rec) {
var guid = rec._storeKey();
var dirty = this.get('_dirtyRecords') || {};
delete dirty[guid];
var count = 0;
for(var elem in dirty){
count = count + 1;
}
if (count === 0) {
this.set('hasChanged', NO);
}
},
/**
Since records are cached by primaryKey, whenever that key changes we need
to re-cache it in the proper place
@param {string} oldkey Previous primary key
@param {string} newkey New primary key
@param {SC.Record} rec The object to relocate
@returns {SC.Record} The record passed in
**/
relocateRecord: function( oldkey, newkey, rec )
{
if (!this._quickCache) return rec;
var classKey = rec._storeKey();
var records = this._quickCache[classKey] || {};
records[newkey] = rec;
delete records[oldkey];
this._quickCache[classKey] = records;
return rec;
},
/**
You can pass any number of condition hashes to this, ending with a
recordType. It will AND the results of each condition hash.
*/
findRecords: function() {
var allConditions = SC.$A(arguments) ;
var recordType = allConditions.pop() ;
var guid = recordType._storeKey() ;
// initial set of records.
var records = this._records[guid] ;
while(allConditions.length > 0) {
var conditions = allConditions.pop() ;
var ret = [] ; var loc = (records) ? records.length : 0;
while(--loc >= 0) {
var rec = records[loc] ;
if ((rec.constructor == recordType) || (rec.constructor.coreRecordType == recordType)) {
if (rec.matchConditions(conditions)) ret.push(rec) ;
}
}
records = ret ;
}
// clone records...
return SC.$A(records) ;
},
// private method used by Record and Store. Returns null if the record does not exist.
_getRecordFor: function(pkValue,recordType) {
var guid = recordType._storeKey() ;
var records = (this._quickCache) ? this._quickCache[guid] : null;
var ret = (records) ? records[pkValue] : null ;
return ret ;
},
/**
finds the record with the primary key value. If the record does not
exist, creates it.
*/
getRecordFor: function(pkValue,recordType,dontAutoaddRecord) {
var ret = this._getRecordFor(pkValue,recordType) ;
if (!ret) {
var opts = {}; opts[recordType.primaryKey()] = pkValue;
ret = recordType.create(opts) ;
if (dontAutoaddRecord) {
ret.needsAddToStore = true ;
} else this.addRecord(ret) ;
}
return ret ;
},
/** @property
Returns an array of all records in the store. Mostly used for storing.
*/
records: function() {
var ret = [] ;
if (this._quickCache) {
for(var key in this._quickCache) {
var recs = this._quickCache[key] ;
for(var recKey in recs) {
ret.push(recs[recKey]) ;
}
}
}
return ret ;
}.property(),
// ....................................
// Collection Helpers
//
addCollection: function(collection) {
var guid = collection.recordType._storeKey() ;
var collections = this._collections[guid] || [] ;
collections.push(collection) ;
this._collections[guid] = collections ;
},
removeCollection: function(collection) {
var guid = collection.recordType._storeKey() ;
var collections = this._collections[guid] || [] ;
collections = collections.without(collection) ;
this._collections[guid] = collections ;
},
listFor: function(opts) {
var conditions = opts.conditions || {} ;
var order = opts.order || ['guid'] ;
var records = this.findRecords(conditions,opts.recordType) ;
var count = records.length ;
// sort
records = records.sort(function(a,b) { return a.compareTo(b,order); }) ;
// slice if needed.
if (opts.limit && (opts.limit > 0)) {
var start = (opts.offset) ? opts.offset : 0 ;
var end = start + opts.limit ;
records = records.slice(start,end) ;
}
// now run callback.
if (opts.callback) opts.callback(records,count) ;
},
dirtyRecords: function(){
var dirty = this.get('_dirtyRecords') || {};
var returnAry = [];
for(var elem in dirty){
if (dirty[elem] !== null) returnAry.push(dirty[elem]);
}
return returnAry;
}.property(),
// ....................................
// PRIVATE
//
_records: {}, _changedRecords: {}, _collections: {}, _dirtyRecords: {},
/** @private
called whenever properties on a record change.
*/
recordDidChange: function(rec) {
// add to changed records. This will eventually notify collections.
var guid = rec._storeKey(),
changed = this.get('_changedRecords') || {},
dirty = this.get('_dirtyRecords') || {},
records = changed[guid] || {} ;
records[SC.guidFor(rec)] = rec ;
changed[guid] = records ;
this.set('_changedRecords',changed) ;
// Set the global record changes
dirty[guid] = rec;
this.set('_dirtyRecords', dirty);
this.set('hasChanged', YES);
this._changedRecordsObserver() ;
},
// invoked whenever the changedRecords hash is updated. This will notify
// collections.
_changedRecordsObserver: function() {
// process changedRecords to notify collections.
for(var guid in this._changedRecords) {
var collections = this._collections[guid] ;
if (collections && collections.length>0) {
// collect records into array. Faster than using uniq.
var records = [] ;
for(var key in this._changedRecords[guid]) {
records.push(this._changedRecords[guid][key]) ;
}
var cloc = collections.length ;
while(--cloc >= 0) {
// for each collection watching this type of record, notify.
var col = collections[cloc] ;
col.beginPropertyChanges() ;
try {
// for each record...
var rloc = records.length ;
while(--rloc >= 0) {
var record = records[rloc] ;
// notify only if record type matches.
if (col.recordType == record.constructor) {
col.recordDidChange(record) ;
}
}
}
catch (e) {
console.log('EXCEPTION: While notifying collection') ;
}
col.endPropertyChanges() ;
}
}
}
// then clear changed records to start again.
this._changedRecords = {} ;
//this.set('hasChanged', NO);
}.observes('_changedRecords')
}) ;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment