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