1 /** Provides a cross-browser persisted storage layer using various storage adapters. 2 * @constructor Asynchronously creates an instance of LocalStorage. 3 * @param {String} id Required identifier for the specific storage instance. 4 * @param {Object} [options] Hashmap of available config options (see description) 5 * @param {Object} [options.adapter=auto] Attempt to use a specific adapter. If unset, the best adapter is automatically used (useBest=true). 6 * @param {Object} [options.useBest=true] Attempt to use the best adapter available, unless an adapter is manually specified and loads successfully. 7 * @param {Object} [options.restore=true] Attempt to restore the data immediately. 8 * @param {Function} [callback] Gets passed a reference to the instance after the initial restore() has completed. 9 */ 10 puredom.LocalStorage = function LocalStorage(id, callback, options) { 11 var self = this; 12 if (typeof arguments[2]==='function') { 13 callback = arguments[2]; 14 options = arguments[1]; 15 } 16 options = options || {}; 17 18 this.id = id; 19 this.adapter = null; 20 this.data = {}; 21 22 if (options.adapter) { 23 this.setAdapter(options.adapter); 24 } 25 if (!this.adapter && options.useBest!==false) { 26 this.useBestAdapter(); 27 } 28 if (this.adapter && options.restore!==false) { 29 this.restore(function() { 30 if (callback) { 31 callback(self); 32 } 33 self = options = null; 34 }); 35 } 36 else if (callback) { 37 callback(self); 38 self = null; 39 } 40 }; 41 42 43 /** The maximum number of milliseconds to wait before committing data to the persistence layer. 44 * @number 45 */ 46 puredom.LocalStorage.prototype.commitDelay = 100; 47 48 49 /** The internal data representation. 50 * @private 51 */ 52 puredom.LocalStorage.prototype.data = {}; 53 54 55 /** Specify the name of a storage adapter the instance should use.<br /> 56 * <strong>Note:</strong> Pay attention to the return value! 57 * Even if you know a given adapter exists, it may fail to load if it is not supported in the 58 * current environment (eg: if window.localStorage doesn't exists, cookies are blocked, etc). 59 * @param {String} type The name of an adapter to use. For a list, see {@link puredom.LocalStorage.adapters} 60 * @returns {Boolean} <code>true</code> if the adapter loaded successfully, <code>false</code> if the specified adapter did not exist or could not be loaded. 61 */ 62 puredom.LocalStorage.prototype.setAdapter = function(type) { 63 var list = this.constructor.adapters, 64 lcType = (type+'').toLowerCase().replace(/adapt[eo]r$/g,''), 65 found = false, 66 foundWorking = false, 67 i; 68 for (i in list) { 69 if (list.hasOwnProperty(i) && (i+'').toLowerCase().replace(/adapt[eo]r$/g,'')===lcType) { 70 found = true; 71 if (list[i].test(this)===true) { 72 foundWorking = true; 73 this.adapterName = type; 74 this.adapter = list[i]; 75 break; 76 } 77 } 78 } 79 if (!found) { 80 puredom.log('puredom.LocalStorage :: Could not find "'+type+'" adapter.'); 81 return false; 82 } 83 if (!foundWorking) { 84 puredom.log('puredom.LocalStorage :: "'+type+'" adapter test() failed: conditions for adapter use not met.'); 85 return false; 86 } 87 return true; 88 }; 89 90 /** Get the name of the active adapter. 91 * @returns {String} The curernt adapter's name 92 */ 93 puredom.LocalStorage.prototype.getAdapter = function() { 94 return this.adapterName; 95 }; 96 97 /** Load whichever adapter works best in the current environment. <br /> 98 * This is determined by querying each adapter to check which are supported, then 99 * selecting the best based on a pre-determined "score", as reported by the adapter.<br /> 100 * <strong>Note:</strong> This method throws a delayed error (does not stop execution) if no adapters are supported in the current environment. 101 */ 102 puredom.LocalStorage.prototype.useBestAdapter = function() { 103 var list = this.constructor.adapters, 104 best, bestName, i; 105 for (i in list) { 106 if (list.hasOwnProperty(i) && i!=='none' && list[i].test(this)===true) { 107 if (!best || (Math.round(best.rating) || 0)<(Math.round(list[i].rating) || 0)) { 108 best = list[i]; 109 bestName = i; 110 } 111 } 112 } 113 if (best) { 114 this.adapterName = bestName; 115 this.adapter = best; 116 } 117 else { 118 setTimeout(function() { 119 throw('puredom.LocalStorage :: Could not find the best adapter.'); 120 }, 1); 121 return false; 122 } 123 return true; 124 }; 125 126 /** Get a namespaced facade of the LocalStorage interface.<br /> 127 * <strong>Tip:</strong> This is a nice way to reduce the number of commits triggered by a large application, 128 * because all namespaces derived from a single LocalStorage instance share the same commit queue. 129 * @param {String} ns A namespace (prefix) to use. Example: <code>"model.session"</code> 130 * @returns {puredom.LocalStorage.NamespacedLocalStorage} An interface identical to {@link puredom.LocalStorage}. 131 */ 132 puredom.LocalStorage.prototype.getNamespace = function(ns) { 133 var self = this, 134 iface; 135 ns = ns + ''; 136 if (ns.substring(0,1)==='.') { 137 ns = ns.substring(1); 138 } 139 if (ns.substring(ns.length-1)==='.') { 140 ns = ns.substring(0, ns.length-1); 141 } 142 iface = puredom.extend(new puredom.LocalStorage.NamespacedLocalStorage(), { 143 getAdapter : function() { 144 return self.getAdapter(); 145 }, 146 /** omg this is so meta */ 147 getNamespace : this.getNamespace, 148 getValue : function(key) { 149 return self.getValue(ns+'.'+key); 150 }, 151 setValue : function(key, value) { 152 self.setValue(ns+'.'+key, value); 153 return this; 154 }, 155 removeKey : function(key) { 156 self.removeKey(ns+'.'+key); 157 return this; 158 }, 159 purge : function() { 160 self.removeKey(ns); 161 return this; 162 }, 163 getData : function() { 164 return self.getValue(ns); 165 }, 166 restore : function(callback) { 167 var proxiedCallback, 168 proxiedContext = this; 169 if (callback) { 170 proxiedCallback = function() { 171 callback(proxiedContext); 172 proxiedContext = proxiedCallback = callback = null; 173 }; 174 } 175 self.restore(proxiedCallback); 176 return this; 177 }, 178 commit : function(callback) { 179 var proxiedCallback, 180 proxiedContext = this; 181 if (callback) { 182 proxiedCallback = function() { 183 callback(proxiedContext); 184 proxiedContext = proxiedCallback = callback = null; 185 }; 186 } 187 self.commit(proxiedCallback); 188 return this; 189 } 190 }); 191 puredom.extend(iface, { 192 get : iface.getValue, 193 set : iface.setValue, 194 remove : iface.removeKey 195 }); 196 return iface; 197 }; 198 199 /** Get the full data object. 200 * @returns {Object} data 201 */ 202 puredom.LocalStorage.prototype.getData = function() { 203 return this.data; 204 }; 205 206 207 /** Get the stored value corresponding to a dot-notated key. 208 * @param {String} key A key, specified in dot-notation. 209 * @returns If <code>key</code> exists, returns the corresponding value, otherwise returns undefined. 210 */ 211 puredom.LocalStorage.prototype.getValue = function(key) { 212 var value = puredom.delve(this.data, key); 213 return value; 214 }; 215 216 /** Set the stored value corresponding to a dot-notated key. <br /> 217 * If the key does not exist, it is created. 218 * @param {String} key A key, specified in dot-notation. 219 * @param {Any} [value] The value to set. If an {Object} or {Array}, its internal values become accessible as dot-notated keys. If <code>null</code> or <code>undefined</code>, the key is removed. 220 * @returns {this} 221 */ 222 puredom.LocalStorage.prototype.setValue = function(key, value) { 223 var node = this.data, 224 keyParts = key.split('.'), 225 i; 226 for (i=0; i<keyParts.length-1; i++) { 227 if (!node.hasOwnProperty(keyParts[i])) { 228 node[keyParts[i]] = {}; 229 } 230 node = node[keyParts[i]]; 231 } 232 if (value===undefined || value===null) { 233 node[keyParts[keyParts.length-1]] = null; 234 delete node[keyParts[keyParts.length-1]]; 235 } 236 else { 237 node[keyParts[keyParts.length-1]] = value; 238 } 239 this.queueCommit(); 240 return this; 241 }; 242 243 /** Remove a key and (its stored value) from the collection. 244 * @param {String} key A key, specified in dot-notation. 245 * @returns {this} 246 */ 247 puredom.LocalStorage.prototype.removeKey = function(key) { 248 this.setValue(key, undefined); 249 return this; 250 }; 251 252 /** Alias of {puredom.LocalStorage#getValue} */ 253 puredom.LocalStorage.prototype.get = puredom.LocalStorage.prototype.getValue; 254 255 /** Alias of {puredom.LocalStorage#setValue} */ 256 puredom.LocalStorage.prototype.set = puredom.LocalStorage.prototype.setValue; 257 258 /** Alias of {puredom.LocalStorage#removeKey} */ 259 puredom.LocalStorage.prototype.remove = puredom.LocalStorage.prototype.removeKey; 260 261 /** Remove all keys/values in the collection. 262 * @returns {this} 263 */ 264 puredom.LocalStorage.prototype.purge = function() { 265 this.data = {}; 266 this.queueCommit(); 267 return this; 268 }; 269 270 /** Restore the collection from its persisted state. 271 * @param {Function} callback Gets called when the restore has completed, passed a reference to the instance. 272 * @returns {this} 273 */ 274 puredom.LocalStorage.prototype.restore = function(callback) { 275 var self = this, 276 data, asyncData; 277 data = this._adapterCall('load', function(r) { 278 self.data = asyncData = r || {}; 279 if (callback) { 280 callback(self); 281 } 282 self = null; 283 }); 284 if (data && !asyncData) { 285 this.data = data; 286 if (callback) { 287 callback(this); 288 } 289 } 290 data = asyncData = null; 291 return this; 292 }; 293 294 /** Save the collection <strong>immediately</strong> using the active persistence adapter.<br /> 295 * This bypasses the default "delayed write" save technique that is implicitly used when interacting with other methods. 296 * @param {Function} callback Gets called when the commit has completed, passed a reference to the instance. 297 * @returns {this} 298 */ 299 puredom.LocalStorage.prototype.commit = function(callback) { 300 var self = this; 301 if (this._commitTimer) { 302 clearTimeout(this._commitTimer); 303 this._commitTimer = null; 304 } 305 this._adapterCall('save', this.data, function() { 306 if (callback) { 307 callback(self); 308 } 309 self = null; 310 }); 311 return this; 312 }; 313 314 /** Queue a commit if one is not already queued. 315 * @private 316 */ 317 puredom.LocalStorage.prototype.queueCommit = function() { 318 var self = this; 319 if (!this._commitTimer) { 320 this._commitTimer = setTimeout(function() { 321 self.commit(); 322 self = null; 323 }, this.commitDelay); 324 } 325 }; 326 327 /** Make a call to the active persistence adapter. 328 * @private 329 * @param {String} func The adapter function to execute 330 * @param args All other arguments are forwarded on to the adapter. 331 * @returns {Any} The adapter method's return value. 332 */ 333 puredom.LocalStorage.prototype._adapterCall = function(func, args) { 334 if (this.adapter && this.adapter[func]) { 335 return this.adapter[func].apply(this.adapter, [this].concat(puredom.toArray(arguments).slice(1))); 336 } 337 }; 338 339 340 341 342 343 344 345 /** A namespaced facade of the LocalStorage interface. 346 * @augments puredom.LocalStorage 347 * @abstract 348 */ 349 puredom.LocalStorage.NamespacedLocalStorage = function(){}; 350 351 352 /** @namespace A list of registered adapters 353 */ 354 puredom.LocalStorage.adapters = {}; 355 356 357 /** Register a storage adapter. 358 * @param {String} name A name for the adapter. Used by {@link puredom.LocalStorage#setAdapter} and {@link puredom.LocalStorage#getAdapter}. 359 * @param {Object} adapter The adapter itself. 360 * @public 361 */ 362 puredom.LocalStorage.addAdapter = function(name, adapter) { 363 if (!adapter.save) { 364 throw('puredom.LocalStorage :: Adapter "'+name+'" attempted to register, but does not provide a save() method.'); 365 } 366 else if (!adapter.load) { 367 throw('puredom.LocalStorage :: Adapter "'+name+'" attempted to register, but does not provide a load() method.'); 368 } 369 else { 370 this.adapters[name] = adapter; 371 } 372 }; 373 374 375 /** @class Abstract storage adapter interface. */ 376 puredom.LocalStorage.adapters.none = function() {}; 377 378 puredom.extend(puredom.LocalStorage.adapters.none.prototype, /** @lends puredom.LocalStorage.adapters.none */ { 379 380 /** The default ID to use for database storage. <br /> 381 * This is used as a fallback in cases {@link puredom.LocalStorage#id} does not exist. 382 */ 383 defaultName : 'db', 384 385 /** An adapter rating from 0-100. Ratings should be based on <strong>speed</strong> and <strong>storage capacity</strong>. <br /> 386 * It is also possible to produce a dynamic rating value based on the current environment, though this is not recommended in most cases. 387 */ 388 rating : 0, 389 390 /** Tests if the adapter is supported in the current environment. 391 * @param {puredom.LocalStorage} storage The parent storage instance. 392 * @returns {Boolean} isSupported 393 */ 394 test : function(storage) { 395 return false; 396 }, 397 398 /** Load all persisted data. 399 * @param {puredom.LocalStorage} storage The parent storage instance. 400 * @param {Function} callback A function to call once the data has been loaded. Expects a JSON object. 401 */ 402 load : function(storage, callback) { 403 if (callback) { 404 callback(); 405 } 406 }, 407 408 /** Save all data to the persistence layer. 409 * @param {puredom.LocalStorage} storage The parent storage instance. 410 * @param {Object} data The JSON data to be saved. 411 * @param {Function} callback A function to call once the data has been saved. Expects a {Boolean} value indicating if the save was successful. 412 */ 413 save : function(storage, data, callback) { 414 if (callback) { 415 callback(false); 416 } 417 } 418 419 }); 420 421 422 423 424 425