Skip to content

Instantly share code, notes, and snippets.

@rgrove
Created October 16, 2009 05:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rgrove/211590 to your computer and use it in GitHub Desktop.
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).
/**
* 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