YUI Cookie Storage Engine

In the YUI Storage Utility article, we covered the new utility method added into YUI 2.8 for storing large amounts of data client-side using JavaScript. The utility uses a variety of engines to make this possible, and was written to be easily extensible, so developers can write and add their own engines. This article will walk through how to create a simple engine using browser cookies.

For starters, I do not recommending using this engine on a production environment, because cookies have severe limitations, both browser- and server-side (see file documentation). However, since every browser supports cookies, they provide a simple framework for wiring up a simple engine and illustrating how to develop one.

Example 1: Setup

	// internal shorthand
var Y = YAHOO.util,
	YC = Y.Cookie,
	YL = YAHOO.lang,

	// constants
	COOKIE_PREFIX = SEC_,
	MAX_BYTE_SIZE = 4000,
	MAX_COOKIE_SIZE = 20,

	// local variables
	_driver = document.cookie;


First setup the engine by referencing YUI libraries, creating engine constants, and a pointer to the engine. In this case, and the HTML 5 engine, the engine is relatively simple because it uses standard DOM objects. See the StorageEngineSWF and StorageEngineGears for a more complicated engine example.

Example 2: Constructor

Y.StorageEngineCookie = function(location, conf) {
	var _this = this, cookies;
	Y.StorageEngineCookie.superclass.constructor.call(_this, location, Y.StorageEngineCookie.ENGINE_NAME, conf);

	cookies = YC.getSubs(COOKIE_PREFIX + location);

	if (cookies) {
		for (var k in cookies) {
			if (YL.isString(cookies[k])) {
				_this._addKey(k);
			}
		}
	}

	_this.length = _this._keys.length;
	YL.later(250, _this, function() { // temporary solution so that CE_READY can be subscribed to after this object is created
		_this.fireEvent(_this.CE_READY);
	});
};

The constructor calls its superclass, handles any driver initilization (the underlying technology), key caching (if necessary, not used for HTML 5 engine), setting the engine length, and firing the CE_READY event. The superclass is called with all the parameters passed into the constructor, plus the ENGINE_NAME, so that the getName() function of an instantiated engine returns the correct engine name. In this case the driver does not need to anything initialized, because document.cookie is provided automatically by the browser. Both the SWF and Gears engines create a singleton pointer to their respective technology, which will be shared by all instances of those engines.

For this engine we are using one cookie to manage session storage and another to manage locale storage. We could fragment data across multiple cookies, but due to cookie limitations, it is better we limit the data storage to just 4kb and one cookie each. The key/value pairs are stored on the cookie using the setSub() function of the YUI Cookie Utility. This allows us to fetch all the cookies in the constructor using the YAHOO.util.Cookie.getSubs() function. We iterate on these cookies to cache their keys, then update the length of the engine, and finally fire the CE_READY event.

The _this._addKey function is part of an intermediary object (YAHOO.util.StorageEngineKeyed) that descends from YAHOO.util.Storage, with added functionality to handle key caching. The StorageEngineKeyed class is used when it is an expensive operation to read the keys from the engine. By using it we can uniformly cache the keys on all flavors of StorageEngine objects. The StorageEngineCookie could be written without it, as the functions of YAHOO.util.Cookie are sufficient, but I wanted to show how it is used.

Example 3: Extend Storage With Implementation

YL.extend(Y.StorageEngineCookie, Y.StorageEngineKeyed, {
	_clear: function() {
		YC.setSubs(COOKIE_PREFIX + this._location, {});
		this._keys = [];
		this.length = 0;
	},

	_getItem: function(key) {
		var value = YC.getSub(COOKIE_PREFIX + this._location, key);
		return value ? decodeURIComponent(value) : null;
	},

	_key: function(index) {return this._keys[index];},

	_removeItem: function(key) {
		YC.removeSub(COOKIE_PREFIX + this._location, key);
		this._removeKey(key);
	},

	_setItem: function(key, data) {
		if (! this.hasKey(key)) {
			this._addKey(key);
		}

		if (MAX_BYTE_SIZE < (encodeURIComponent(& + key + = + data) + YC.get(COOKIE_PREFIX + this._location)).length) {
			return false;
		}

		var options = {};
		// ensures the non-session data remains stored for up to 10 years
		if (Y.StorageManager.LOCATION_LOCAL === this._location) {
			var date = new Date();
			date.setFullYear(date.getFullYear() + 10);
			options.expires=date;
		}

		YC.setSub(COOKIE_PREFIX + this._location, key, data, options);
		return true;
	}
});

Example 3 has the meat of the engine, the functions that actually read/write from the driver. When the developer calls the public functions of an instantiated engine (such as setItem()) and the values parameters validate, then the corresponding protected implementation methods is called (such as _setItem()). All engines must override these 5 methods. The _setItem() function is the most complex as it add the appropriate key to the cache, evaluates if there is space available before writing, then returns whether the operation was successful. The setItem() will throw a QUOTA_EXCEEDED_ERROR error if _setItem() returns false.

For LOCATION_LOCAL storage we set the cookie to expire in 10 years, each time it is updated, which should be a sufficient duration.

Example 4: Setup Engine Constants

Y.StorageEngineCookie.ENGINE_NAME = cookie;
Y.StorageEngineCookie.isAvailable = function() {
	var testName = YAHOO.util.Cookie,
		testValue = test,
		numberOfCookies;

	if (navigator && ! navigator.cookieEnabled) {return false;} // navigator tells us no
	numberOfCookies = ( + _driver).split(;).length;
	if (numberOfCookies) {return numberOfCookies < MAX_COOKIE_SIZE;} // cookies exists, assume enabled

	YC.set(testName, testValue);

	// evaluate that we can write a cookie, will fail if their are no more cookies as well
	if (testValue === YC.get(testName)) {
		YC.remove(testName);
		return true;
	}

	return false;
};

Y.StorageManager.register(Y.StorageEngineCookie);

An engine needs, at least the constant ENGINE_NAME and the function isAvailable() defined on the constructor of the engine. Both methods will be used by the YAHOO.util.StorageManager to determine if the engine should be used and where it should be cached. The ENGINE_NAME should be unique and not shared by any other engines. Lastly, call YAHOO.util.StorageManager.register and pass in the constructor to make the engine avialable for use.

And that is all there is to it. I believe the two biggest areas for future improvement how the CE_READY event is handled, and to allow the public methods to work asynchronously by accepting callback functions. I think the latter improvement would simplify the API a bit and allow support for drivers like YAHOO&rsquot;s BrowserPlus and even an AJAX driver, to abstract away the server. Here is an example where you can use the cookie driver.