Created
October 16, 2009 05:26
-
-
Save rgrove/211590 to your computer and use it in GitHub Desktop.
A persistent local key/value data store similar to HTML5's localStorage. Works in IE5+, Firefox 2+, Safari 3.1+, and any browser with Google Gears installed (including Chrome).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Implements a persistent local key/value data store similar to HTML5's | |
* localStorage. Should work in IE5+, Firefox 2+, Safari 3.1+, and any browser | |
* with Google Gears installed (including Chrome). Doesn't work in Opera. | |
* | |
* @module storage | |
* @namespace YAHOO.Search | |
* @requires yahoo, event, json | |
*/ | |
(function () { | |
// Shorthand. | |
var d = document, | |
w = window, | |
Y = YAHOO, | |
YS = Y.namespace('Search'), | |
yut = Y.util, | |
JSON = window.JSON || Y.lang.JSON, | |
Event = yut.Event, | |
// -- Private Constants ---------------------------------------------------- | |
DB_NAME = 'ysearch_storage', | |
DB_DISPLAYNAME = 'Yahoo! Search Storage', | |
DB_MAXSIZE = 1048576, | |
DB_VERSION = '1.0', | |
USERDATA_PATH = 'ysearch', | |
USERDATA_NAME = 'data', | |
// -- Private Variables ---------------------------------------------------- | |
data = {}, ready = false, self, storage; | |
// -- Storage Classes ------------------------------------------------------ | |
YS.StorageFullError = function (message) { | |
YS.StorageFullError.superclass.constructor.call(message); | |
this.name = 'StorageFullError'; | |
this.message = message || 'Maximum storage capacity reached'; | |
if (Y.env.ua.ie) { | |
this.description = this.message; | |
} | |
}; | |
Y.lang.extend(YS.StorageFullError, Error); | |
/** | |
* The StorageInterface class defines the interface that all storage | |
* implementations will adhere to. This is also the noop fallback for | |
* browsers that don't support an actual storage implementation. | |
* | |
* @class StorageInterface | |
* @constructor | |
* @private | |
*/ | |
function StorageInterface() { | |
this.onStorageReady.subscribeEvent.subscribe(function (e, args) { | |
var fn = args[0], | |
obj = args[1], | |
overrideScope = args[2]; | |
if (ready && fn) { | |
// Sadly, subscribeEvent is broken in that it doesn't give us | |
// any reliable way of calling the new subscriber exactly the | |
// same way that CustomEvent.fire would. This sucks. So we'll | |
// just have to do our best. | |
if (obj && overrideScope) { | |
fn.call(obj); | |
} else { | |
fn.call(window, obj); | |
} | |
} | |
}); | |
} | |
// TODO: storage events | |
// TODO: event when approaching storage limit | |
StorageInterface.prototype = { | |
// -- Public Events ---------------------------------------------------- | |
/** | |
* Fired when the storage interface is loaded and ready for use. | |
* | |
* @event onStorageReady | |
* @type CustomEvent | |
*/ | |
onStorageReady: new yut.CustomEvent('storageReady'), | |
// -- Public Methods --------------------------------------------------- | |
/** | |
* Removes all items from the data store. | |
* | |
* @method clear | |
*/ | |
clear: function () {}, | |
/** | |
* Returns the item with the specified key, or <code>null</code> if the | |
* item was not found. | |
* | |
* @method getItem | |
* @param {String} key | |
* @param {bool} json (optional) <code>true</code> if the item is a JSON | |
* string and should be parsed before being returned | |
* @return {Object|null} item or <code>null</code> if not found | |
*/ | |
getItem: function (key, json) { return null; }, | |
/** | |
* Returns the number of items in the data store. | |
* | |
* @method length | |
* @return {Number} number of items in the data store | |
*/ | |
length: function () { return 0; }, | |
/** | |
* Removes the item with the specified key. | |
* | |
* @method removeItem | |
* @param {String} key | |
*/ | |
removeItem: function (key) {}, | |
/** | |
* Stores an item under the specified key. If the key already exists in | |
* the data store, it will be replaced. | |
* | |
* @method setItem | |
* @param {String} key | |
* @param {Object} value | |
* @param {bool} json (optional) <code>true</code> if the item should be | |
* serialized to a JSON string before being stored | |
*/ | |
setItem: function (key, value, json) {} | |
}; | |
/** | |
* The DatabaseStorage class provides a SQLite-based local data store for | |
* Safari 3.1 and 3.2. | |
* | |
* @class DatabaseStorage | |
* @uses StorageInterface | |
* @constructor | |
* @private | |
*/ | |
function DatabaseStorage() { | |
self = this; | |
StorageInterface.call(self); | |
self._open(); | |
self._create(); | |
} | |
DatabaseStorage.prototype = { | |
clear: function () { | |
data = {}; | |
self._save(); | |
}, | |
getItem: function (key, json) { | |
return data.hasOwnProperty(key) ? data[key] : null; | |
}, | |
length: function () { | |
var count = 0, key; | |
for (key in data) { | |
if (data.hasOwnProperty(key)) { | |
count += 1; | |
} | |
} | |
return count; | |
}, | |
removeItem: function (key) { | |
delete(data[key]); | |
self._save(); | |
}, | |
setItem: function (key, value, json) { | |
data[key] = value; | |
self._save(); | |
}, | |
_create: function () { | |
storage.transaction(function (t) { | |
t.executeSql("CREATE TABLE IF NOT EXISTS ysearch_storage(name TEXT PRIMARY KEY, value TEXT NOT NULL)"); | |
t.executeSql("SELECT value FROM ysearch_storage WHERE name = 'data'", [], self._load); | |
}); | |
}, | |
_load: function (t, results) { | |
if (results.rows.length) { | |
try { | |
data = JSON.parse(results.rows.item(0).value); | |
} catch (e) { | |
data = {}; | |
} | |
} | |
ready = true; | |
self.onStorageReady.fire(); | |
}, | |
_open: function () { | |
storage = w.openDatabase(DB_NAME, DB_VERSION, DB_DISPLAYNAME, DB_MAXSIZE); | |
}, | |
_save: function () { | |
storage.transaction(function (t) { | |
t.executeSql("REPLACE INTO ysearch_storage (name, value) VALUES ('data', ?)", [JSON.stringify(data)]); | |
}); | |
} | |
}; | |
/** | |
* The GearsStorage class provides a Google Gears-based local data store for | |
* Google Chrome and any browser with Google Gears installed. | |
* | |
* @class GearsStorage | |
* @uses DatabaseStorage | |
* @constructor | |
* @private | |
*/ | |
function GearsStorage() { | |
self = this; | |
StorageInterface.call(self); | |
self._open(); | |
self._create(); | |
} | |
GearsStorage.prototype = { | |
_create: function () { | |
storage.execute("CREATE TABLE IF NOT EXISTS ysearch_storage(name TEXT PRIMARY KEY, value TEXT NOT NULL)"); | |
self._load(storage.execute("SELECT value FROM ysearch_storage WHERE name = 'data'")); | |
}, | |
_load: function (results) { | |
if (results.isValidRow() && results.fieldCount()) { | |
try { | |
data = JSON.parse(results.field(0)); | |
} catch (e) { | |
data = {}; | |
} | |
} | |
ready = true; | |
self.onStorageReady.fire(); | |
}, | |
_open: function () { | |
storage = google.gears.factory.create('beta.database'); | |
storage.open(DB_NAME); | |
}, | |
_save: function () { | |
var retries = 0, | |
store = function () { | |
try { | |
storage.execute("REPLACE INTO ysearch_storage (name, value) VALUES ('data', ?)", [JSON.stringify(data)]); | |
} catch (e) { | |
// Gears database write operations can fail if multiple | |
// processes attempt to write to the database at the | |
// same time. Since Gears apparently can't be bothered | |
// to handle this case on its own like any reasonable | |
// database would, we have to deal with it. | |
if (retries > 2) { | |
throw e; | |
} | |
setTimeout(store, 50 * (retries += 1)); | |
} | |
}; | |
store(); | |
} | |
}; | |
/** | |
* The GeckoStorage class provides a globalStorage-based local data store | |
* for Firefox 2 and 3.0. | |
* | |
* @class GeckoStorage | |
* @uses HTML5Storage | |
* @constructor | |
* @private | |
*/ | |
function GeckoStorage() { | |
StorageInterface.call(this); | |
storage = w.globalStorage[w.location.hostname]; | |
ready = true; | |
this.onStorageReady.fire(); | |
} | |
GeckoStorage.prototype = { | |
clear: function () { | |
for (var key in storage) { | |
if (storage.hasOwnProperty(key)) { | |
storage.removeItem(key); | |
} | |
} | |
}, | |
getItem: function (key, json) { | |
try { | |
return json ? JSON.parse(storage[key].value) : | |
storage[key].value; | |
} catch (e) { | |
return null; | |
} | |
} | |
}; | |
/** | |
* The HTML5Storage class provides a localStorage-based local data store for | |
* browsers that support HTML5 storage (currently IE8, Firefox 3.5, and | |
* Safari 4). | |
* | |
* @class HTML5Storage | |
* @uses StorageInterface | |
* @constructor | |
* @private | |
*/ | |
function HTML5Storage() { | |
StorageInterface.call(this); | |
storage = w.localStorage; | |
ready = true; | |
this.onStorageReady.fire(); | |
} | |
HTML5Storage.prototype = { | |
clear: function () { | |
storage.clear(); | |
}, | |
getItem: function (key, json) { | |
try { | |
return json ? JSON.parse(storage.getItem(key)) : | |
storage.getItem(key); | |
} catch (e) { | |
return null; | |
} | |
}, | |
length: function () { | |
return storage.length; | |
}, | |
removeItem: function (key) { | |
storage.removeItem(key); | |
}, | |
setItem: function (key, value, json) { | |
storage.setItem(key, json ? JSON.stringify(value) : value); | |
} | |
}; | |
/** | |
* The UserDataStorage class provides a userData-based local data store for | |
* IE5, 6, and 7. | |
* | |
* @class UserDataStorage | |
* @uses DatabaseStorage | |
* @constructor | |
* @private | |
*/ | |
function UserDataStorage() { | |
self = this; | |
StorageInterface.call(self); | |
storage = d.createElement('span'); | |
storage.addBehavior('#default#userData'); | |
Event.onDOMReady(function () { | |
d.body.appendChild(storage); | |
storage.load(USERDATA_PATH); | |
try { | |
data = JSON.parse(storage.getAttribute(USERDATA_NAME)); | |
} catch (e) { | |
data = {}; | |
} | |
ready = true; | |
self.onStorageReady.fire(); | |
}); | |
} | |
UserDataStorage.prototype = { | |
_save: function () { | |
var _data = JSON.stringify(data); | |
try { | |
storage.setAttribute(USERDATA_NAME, _data); | |
storage.save(USERDATA_PATH); | |
} catch (e) { | |
throw new YS.StorageFullError(); | |
} | |
} | |
}; | |
/** | |
* Provides a persistent local key/value data store similar to HTML5's | |
* localStorage. | |
* | |
* @class Storage | |
* @uses StorageInterface | |
* @static | |
*/ | |
Y.lang.augmentProto(DatabaseStorage, StorageInterface); | |
Y.lang.augmentProto(HTML5Storage, StorageInterface); | |
Y.lang.augmentProto(GearsStorage, DatabaseStorage); | |
Y.lang.augmentProto(GeckoStorage, HTML5Storage); | |
Y.lang.augmentProto(UserDataStorage, DatabaseStorage); | |
if (w.localStorage) { | |
YS.Storage = new HTML5Storage(); | |
} else if (w.globalStorage) { | |
YS.Storage = new GeckoStorage(); | |
} else if (w.openDatabase && navigator.userAgent.indexOf('Chrome') === -1) { | |
YS.Storage = new DatabaseStorage(); | |
} else if (w.google && w.google.gears) { | |
YS.Storage = new GearsStorage(); | |
} else if (Y.env.ua.ie >= 5) { | |
YS.Storage = new UserDataStorage(); | |
} else { | |
// This browser doesn't support any of the actual storage | |
// implementations, so we'll give it the noop interface. | |
YS.Storage = new StorageInterface(); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment