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('data_sources/data_source'); 9 sc_require('models/record'); 10 11 /** @class 12 13 TODO: Describe Class 14 15 @extends SC.DataSource 16 @since SproutCore 1.0 17 */ 18 SC.FixturesDataSource = SC.DataSource.extend( 19 /** @scope SC.FixturesDataSource.prototype */ { 20 21 /** 22 If YES then the data source will asynchronously respond to data requests 23 from the server. If you plan to replace the fixture data source with a 24 data source that talks to a real remote server (using Ajax for example), 25 you should leave this property set to YES so that Fixtures source will 26 more accurately simulate your remote data source. 27 28 If you plan to replace this data source with something that works with 29 local storage, for example, then you should set this property to NO to 30 accurately simulate the behavior of your actual data source. 31 32 @type Boolean 33 */ 34 simulateRemoteResponse: NO, 35 36 /** 37 If you set simulateRemoteResponse to YES, then the fixtures source will 38 assume a response latency from your server equal to the msec specified 39 here. You should tune this to simulate latency based on the expected 40 performance of your server network. Here are some good guidelines: 41 42 - 500: Simulates a basic server written in PHP, Ruby, or Python (not twisted) without a CDN in front for caching. 43 - 250: (Default) simulates the average latency needed to go back to your origin server from anywhere in the world. assumes your servers itself will respond to requests < 50 msec 44 - 100: simulates the latency to a "nearby" server (i.e. same part of the world). Suitable for simulating locally hosted servers or servers with multiple data centers around the world. 45 - 50: simulates the latency to an edge cache node when using a CDN. Life is really good if you can afford this kind of setup. 46 47 @type Number 48 */ 49 latency: 50, 50 51 // .......................................................... 52 // CANCELLING 53 // 54 55 /** @private */ 56 cancel: function(store, storeKeys) { 57 return NO; 58 }, 59 60 61 // .......................................................... 62 // FETCHING 63 // 64 65 /** @private */ 66 fetch: function(store, query) { 67 68 // can only handle local queries out of the box 69 if (query.get('location') !== SC.Query.LOCAL) { 70 SC.throw('SC.Fixture data source can only fetch local queries'); 71 } 72 73 if (!query.get('recordType') && !query.get('recordTypes')) { 74 SC.throw('SC.Fixture data source can only fetch queries with one or more record types'); 75 } 76 77 if (this.get('simulateRemoteResponse')) { 78 this.invokeLater(this._fetch, this.get('latency'), store, query); 79 80 } else this._fetch(store, query); 81 }, 82 83 /** @private 84 Actually performs the fetch. 85 */ 86 _fetch: function(store, query) { 87 88 // NOTE: Assumes recordType or recordTypes is defined. checked in fetch() 89 var recordType = query.get('recordType'), 90 recordTypes = query.get('recordTypes') || [recordType]; 91 92 // load fixtures for each recordType 93 recordTypes.forEach(function(recordType) { 94 if (SC.typeOf(recordType) === SC.T_STRING) { 95 recordType = SC.objectForPropertyPath(recordType); 96 } 97 98 if (recordType) this.loadFixturesFor(store, recordType); 99 }, this); 100 101 // notify that query has now loaded - puts it into a READY state 102 store.dataSourceDidFetchQuery(query); 103 }, 104 105 // .......................................................... 106 // RETRIEVING 107 // 108 109 /** @private */ 110 retrieveRecords: function(store, storeKeys) { 111 // first let's see if the fixture data source can handle any of the 112 // storeKeys 113 var latency = this.get('latency'), 114 ret = this.hasFixturesFor(storeKeys) ; 115 if (!ret) return ret ; 116 117 if (this.get('simulateRemoteResponse')) { 118 this.invokeLater(this._retrieveRecords, latency, store, storeKeys); 119 } else this._retrieveRecords(store, storeKeys); 120 121 return ret ; 122 }, 123 124 _retrieveRecords: function(store, storeKeys) { 125 126 storeKeys.forEach(function(storeKey) { 127 var ret = [], 128 recordType = SC.Store.recordTypeFor(storeKey), 129 id = store.idFor(storeKey), 130 hash = this.fixtureForStoreKey(store, storeKey); 131 ret.push(storeKey); 132 store.dataSourceDidComplete(storeKey, hash, id); 133 }, this); 134 }, 135 136 // .......................................................... 137 // UPDATE 138 // 139 140 /** @private */ 141 updateRecords: function(store, storeKeys, params) { 142 // first let's see if the fixture data source can handle any of the 143 // storeKeys 144 var latency = this.get('latency'), 145 ret = this.hasFixturesFor(storeKeys) ; 146 if (!ret) return ret ; 147 148 if (this.get('simulateRemoteResponse')) { 149 this.invokeLater(this._updateRecords, latency, store, storeKeys); 150 } else this._updateRecords(store, storeKeys); 151 152 return ret ; 153 }, 154 155 _updateRecords: function(store, storeKeys) { 156 storeKeys.forEach(function(storeKey) { 157 var hash = store.readDataHash(storeKey); 158 this.setFixtureForStoreKey(store, storeKey, hash); 159 store.dataSourceDidComplete(storeKey); 160 }, this); 161 }, 162 163 164 // .......................................................... 165 // CREATE RECORDS 166 // 167 168 /** @private */ 169 createRecords: function(store, storeKeys, params) { 170 // first let's see if the fixture data source can handle any of the 171 // storeKeys 172 var latency = this.get('latency'); 173 174 if (this.get('simulateRemoteResponse')) { 175 this.invokeLater(this._createRecords, latency, store, storeKeys); 176 } else this._createRecords(store, storeKeys); 177 178 return YES ; 179 }, 180 181 _createRecords: function(store, storeKeys) { 182 storeKeys.forEach(function(storeKey) { 183 var id = store.idFor(storeKey), 184 recordType = store.recordTypeFor(storeKey), 185 dataHash = store.readDataHash(storeKey), 186 fixtures = this.fixturesFor(recordType); 187 188 if (!id) id = this.generateIdFor(recordType, dataHash, store, storeKey); 189 this._invalidateCachesFor(recordType, storeKey, id); 190 fixtures[id] = dataHash; 191 192 store.dataSourceDidComplete(storeKey, null, id); 193 }, this); 194 }, 195 196 // .......................................................... 197 // DESTROY RECORDS 198 // 199 200 /** @private */ 201 destroyRecords: function(store, storeKeys, params) { 202 // first let's see if the fixture data source can handle any of the 203 // storeKeys 204 var latency = this.get('latency'), 205 ret = this.hasFixturesFor(storeKeys) ; 206 if (!ret) return ret ; 207 208 if (this.get('simulateRemoteResponse')) { 209 this.invokeLater(this._destroyRecords, latency, store, storeKeys); 210 } else this._destroyRecords(store, storeKeys); 211 212 return ret ; 213 }, 214 215 216 _destroyRecords: function(store, storeKeys) { 217 storeKeys.forEach(function(storeKey) { 218 var id = store.idFor(storeKey), 219 recordType = store.recordTypeFor(storeKey), 220 fixtures = this.fixturesFor(recordType); 221 222 this._invalidateCachesFor(recordType, storeKey, id); 223 if (id) delete fixtures[id]; 224 store.dataSourceDidDestroy(storeKey); 225 }, this); 226 }, 227 228 // .......................................................... 229 // INTERNAL METHODS/PRIMITIVES 230 // 231 232 /** 233 Load fixtures for a given fetchKey into the store 234 and push it to the ret array. 235 236 @param {SC.Store} store the store to load into 237 @param {SC.Record} recordType the record type to load 238 @param {SC.Array} ret is passed, array to add loaded storeKeys to. 239 @returns {SC.FixturesDataSource} receiver 240 */ 241 loadFixturesFor: function(store, recordType, ret) { 242 var hashes = [], 243 dataHashes, i, storeKey ; 244 245 dataHashes = this.fixturesFor(recordType); 246 247 for(i in dataHashes){ 248 storeKey = recordType.storeKeyFor(i); 249 if (store.peekStatus(storeKey) === SC.Record.EMPTY) { 250 hashes.push(dataHashes[i]); 251 } 252 if (ret) ret.push(storeKey); 253 } 254 255 // only load records that were not already loaded to avoid infinite loops 256 if (hashes && hashes.length>0) store.loadRecords(recordType, hashes); 257 258 return this ; 259 }, 260 261 262 /** 263 Generates an id for the passed record type. You can override this if 264 needed. The default generates a storekey and formats it as a string. 265 266 @param {Class} recordType Subclass of SC.Record 267 @param {Hash} dataHash the data hash for the record 268 @param {SC.Store} store the store 269 @param {Number} storeKey store key for the item 270 @returns {String} 271 */ 272 generateIdFor: function(recordType, dataHash, store, storeKey) { 273 return "@id%@".fmt(SC.Store.generateStoreKey()); 274 }, 275 276 /** 277 Based on the storeKey it returns the specified fixtures 278 279 @param {SC.Store} store the store 280 @param {Number} storeKey the storeKey 281 @returns {Hash} data hash or null 282 */ 283 fixtureForStoreKey: function(store, storeKey) { 284 var id = store.idFor(storeKey), 285 recordType = store.recordTypeFor(storeKey), 286 fixtures = this.fixturesFor(recordType); 287 return fixtures ? fixtures[id] : null; 288 }, 289 290 /** 291 Update the data hash fixture for the named store key. 292 293 @param {SC.Store} store the store 294 @param {Number} storeKey the storeKey 295 @param {Hash} dataHash 296 @returns {SC.FixturesDataSource} receiver 297 */ 298 setFixtureForStoreKey: function(store, storeKey, dataHash) { 299 var id = store.idFor(storeKey), 300 recordType = store.recordTypeFor(storeKey), 301 fixtures = this.fixturesFor(recordType); 302 this._invalidateCachesFor(recordType, storeKey, id); 303 fixtures[id] = dataHash; 304 return this ; 305 }, 306 307 /** 308 Get the fixtures for the passed record type and prepare them if needed. 309 Return cached value when complete. 310 311 @param {SC.Record} recordType 312 @returns {Hash} data hashes 313 */ 314 fixturesFor: function(recordType) { 315 // get basic fixtures hash. 316 if (!this._fixtures) this._fixtures = {}; 317 var fixtures = this._fixtures[SC.guidFor(recordType)]; 318 if (fixtures) return fixtures ; 319 320 // need to load fixtures. 321 var dataHashes = recordType ? recordType.FIXTURES : null, 322 len = dataHashes ? dataHashes.length : 0, 323 primaryKey = recordType ? recordType.prototype.primaryKey : 'guid', 324 idx, dataHash, id ; 325 326 this._fixtures[SC.guidFor(recordType)] = fixtures = {} ; 327 for(idx=0;idx<len;idx++) { 328 dataHash = dataHashes[idx]; 329 id = dataHash[primaryKey]; 330 if (!id) id = this.generateIdFor(recordType, dataHash); 331 fixtures[id] = dataHash; 332 } 333 return fixtures; 334 }, 335 336 /** 337 Returns YES if fixtures for a given recordType have already been loaded 338 339 @param {SC.Record} recordType 340 @returns {Boolean} storeKeys 341 */ 342 fixturesLoadedFor: function(recordType) { 343 if (!this._fixtures) return NO; 344 var ret = [], fixtures = this._fixtures[SC.guidFor(recordType)]; 345 return fixtures ? YES: NO; 346 }, 347 348 /** 349 Resets the fixtures to their original values. 350 351 @returns {SC.FixturesDataSource} receiver 352 */ 353 reset: function(){ 354 this._fixtures = null; 355 return this; 356 }, 357 358 /** 359 Returns YES or SC.MIXED_STATE if one or more of the storeKeys can be 360 handled by the fixture data source. 361 362 @param {Array} storeKeys the store keys 363 @returns {Boolean} YES if all handled, MIXED_STATE if some handled 364 */ 365 hasFixturesFor: function(storeKeys) { 366 var ret = NO ; 367 storeKeys.forEach(function(storeKey) { 368 if (ret !== SC.MIXED_STATE) { 369 var recordType = SC.Store.recordTypeFor(storeKey), 370 fixtures = recordType ? recordType.FIXTURES : null ; 371 if (fixtures && fixtures.length && fixtures.length>0) { 372 if (ret === NO) ret = YES ; 373 } else if (ret === YES) ret = SC.MIXED_STATE ; 374 } 375 }, this); 376 377 return ret ; 378 }, 379 380 /** @private 381 Invalidates any internal caches based on the recordType and optional 382 other parameters. Currently this only invalidates the storeKeyCache used 383 for fetch, but it could invalidate others later as well. 384 385 @param {SC.Record} recordType the type of record modified 386 @param {Number} storeKey optional store key 387 @param {String} id optional record id 388 @returns {SC.FixturesDataSource} receiver 389 */ 390 _invalidateCachesFor: function(recordType, storeKey, id) { 391 var cache = this._storeKeyCache; 392 if (cache) delete cache[SC.guidFor(recordType)]; 393 return this ; 394 } 395 396 }); 397 398 /** 399 Default fixtures instance for use in applications. 400 401 @property {SC.FixturesDataSource} 402 */ 403 SC.Record.fixtures = SC.FixturesDataSource.create(); 404