Data Singleton for AJAX Requests

Although invented years ago, it is the emergence of AJAX over the last few years that has allowed the development of todays powerful, web applications. And while I love what I can do with AJAX, working with asynchronous requests can be tricky. YUI and other frameworks have done a great job allowing for callbacks functions and custom events, so that you do not have to worry as much about null data pointers and managing the request timing. However, I have always believed that there was a better way. That belief led me to develop an Object, I have called DataSingleton, which fetches and manages a singleton instance of all JSON data that I need to retrieve from the server.

On Mint.com I frequently have to fetch JSON data from the server. I was using YUIs Connection.js with a callback function for each request. Then when the request returned, I would populate a scoped instance of the JSON object, before performing a series of carefully choreographed page/JavaScript updates. While this works, it becomes more and more difficult as you modularize your JavaScript. One starts to have dozens of custom events and or accessor functions on objects, so that the data can be passed around between them; when it would be much simpler to just have one function to call, which always gives you the most up-to-date version of that JSON object, no matter where you are, who last updated that object, or whether it is in the middle of an AJAX request or not.

Three solutions came to mind when I thought about this problem: sleep the thread, use a synchronous request, or use the command pattern. Since, JavaScript does not have a sleep function and any attempt to emulate it (short of using the command pattern) has failed, the thread sleep method is out. A synchronous request would work, but it would also tie up the JavaScript processor until the request returned, and make the page temporarily unavailable to any users… not a good idea. So, we are left with the command pattern, where we pass in a function as the action and use the Lazy-Loading Callback Pattern to wait for the AJAX request to complete; or instantly return if the data is not stale.

Example 1: Data Singleton Object

Core.Widget.DataSingleton = (function() {

	// Module Private Variables

	var FETCHING = fetching,
		dataCache = {},
		F = function() {},
		that = null;

	//  Module request namespace

	var req = {

		/**
		 * Is the AJAX request callback for retrieving the popup DOM.
		 * @method get
		 * @param data {Object} Required. The data hash for the ajax request.
		 * @private
		 */
		callback: function(data) {
			if (data.responseText) {
				var text = (data.responseText),
					json = YAHOO.lang.JSON.parse(text);
				
				json.batch(function(o) {
					dataCache[o.id] = o.data;
				});
			}
		},

		/**
		 * Handles the AJAX request to retrieve the popup DOM.
		 * @method get
		 * @param id {String} Required. The name of the DOM node to fetch (url-name).
		 * @param params {Object} Optional. An Object of additional parameters for request.
		 * @private
		 */
		get: function(id, params) {
			var o = Array.is(params) ? params : [];

			// stuff the parameter object
			o[o.length] = jsonIdSet= + id;
			o[o.length] = r= + (new Date()).getTime();
			
			var callback = {
				success: req.callback
			};

			YAHOO.util.Connect.asyncRequest(GET, getJSON.php? + o.join(&), callback, null); 
		}
	};

	//	Public methods and constants
	F.prototype = {

		/**
		 * Attempts to call the process with the cached data, otherwise fetch it.
		 * @method getPopup
		 * @param id {String} Required. The ID of the data to fetch.
		 * @param proc {Function} Required. The process function to pass data into.
		 * @static
		 */
		call: function(id, proc) {
			// verify proper parameters
			if (! (String.is(id) || isType(func, function))) {return null;}

			// if the cache exists, call the process
			if (dataCache[id] && FETCHING !== dataCache[id]) {
				proc(dataCache[id]);
			}
			// cache doesnt exist, initialize it
			else {
				if (FETCHING !== dataCache[id]) {that.init([id]);}

				// set an inverval to check for existence of the data
				//noinspection JSUnusedLocalSymbols
				var pointer = null;
				pointer = setInterval(function() {
					// cache now exists, stop interval and call process
					if (dataCache[id] && FETCHING !== dataCache[id]) {
						clearInterval(pointer);
						proc(dataCache[id]);
					}
				}, 250);
			}
		},

		/**
		 * Initializes the singleton data for this page. Call as early as possible.
		 * @method init
		 * @param idList {Array} Required. The ID list of data to fetch.
		 * @param conf {Object} Optional. The configuration object.
		 * @static
		 */
		init: function(idList, conf) {
			// verify proper parameters
			if (! Array.is(idList)) {return;}
			var cfg = Object.is(conf) ? conf : {};

			// configuration
			if (! Array.is(cfg.params)) {cfg.params = [];}

			idList.batch(function(id) {
				delete dataCache[id];
				dataCache[id] = FETCHING;
			});

			// fetch the ids
			req.get(idList, cfg.params);
		},

		/**
		 * Refetches the data from the server.
		 * @method init
		 * @param idList {Array|String} Required. The ID or list of IDs for data to fetch.
		 * @param conf {Object} Optional. The configuration object.
		 * @static
		 */
		refresh: function(idList, conf) {
			that.init(Array.is(idList) ? idList : [idList], conf);
		}
	};

	that = new F();

	return that;
})();

You will first need to setup a server request that returns your JSON object(s). I use the parameter jsonIdSet to pass a list of JSON data objects to fetch. Your server response should return the following:

Example 2: Server Response

[
	{"id": "task1", "data": {/*JSON data for task 1*/}},
	{"id": "task2", "data": {/*JSON data for task 2*/}},
	{"id": "task3", "data": {/*JSON data for task 3*/}},
	…
]

Before using DataSingleton, you need to initial the data by calling the public init method. You should pass in an array of JSON data ids that your current page needs, and this data will be fetched from the server. The list of JSON ids will become a comma delimited string, which you will need to parse server-side. Client-side, if the data ever becomes stale, use the refresh method to update the data (you can pass either an array of JSON ids or just a single id).

Once you have your server request setup and have initialized your JSON data ids, then you can start making calls for that data. Here is the syntax:

Example 3: Command Call Syntax

var $DS = Core.Widget.DataSingleton;  // creating a shorthand name

$DS.call(task1, function(json) {
	// code that requires json
});

The call method ensures that the singleton instance of the data you are requesting exists, before executing the passed in command function, otherwise it will wait until the data is populated. If you have not initialized the requested JSON data id, it will go ahead and initialize it for you, but it is faster if the data is already initialized (that is why I load it onload). So far the only drawback, I have encountered to using this method, is that the command pattern is cumbersome to use until you familiarize yourself with it. However, this technique has made working with server-driven JSON objects much easier and helped me compact my code.

The DataSingleton Object has a dependency on YUI "connection.js", and my "core.js", "object.js", and "array.js". Here is an example page that uses this code.