Proof Of Concept: Adding Pusher-Powered Update Support To jQuery AJAX
The other day, I was performing an AJAX request that executed a number of laborious tasks on the server. As I was doing this, I thought it would create a very nice user experience if I could provide some sort of piece-wise feedback in the user interface. Unfortunately, there's no elegant way to provide step-updates using jQuery's core ajax() method. You can play around with long-polling AJAX requests; but not only is this buggy, it also interferes with the final response value. Ideally, what I think we need is a way for the server to push notifications back to the client in parallel to the currently executing AJAX request. This way, we can deliver information and allow jQuery to properly handle the success response.
In the past, I've used Pusher to push realtime notifications to the client. This seems like an ideal technology for providing update information in a long-running task. But, is there a way to cleanly and easily integrate Pusher technology with jQuery's ajax() method in order to allow for an "update" event handler? In the following demo, I am exploring a proof-of-concept as to how this can be done in an encapsulated way.
Before we look at the server code or the jQuery plugin code, let's take a look at the client code that makes use of this two-way communication. In the following demo, I am manually launching a long-running AJAX request. During this request, I am using the new "update" event handler to provide mid-request updates to the client.
<!DOCTYPE html>
<html>
<head>
<title>Adding Pusher-Powered Update Support To jQuery AJAX</title>
<script type="text/javascript" src="./jquery-1.4.2.js"></script>
<script src="http://js.pusherapp.com/1.6/pusher.js" type="text/javascript"></script>
<script type="text/javascript" src="./jquery.ajaxpush.js"></script>
<script type="text/javascript">
// When the DOM is ready, initialize scripts.
$(function(){
// Get our doIt link which will launch our ajax.
var link = $( "#doit" );
// Bind to the click handler to launch the ajax request
// with the update functionality.
link.click(
function( event ){
// Prevent the default event.
event.preventDefault();
// Launch AJAX request - notice that we are
// supplying the UPDATE event handler. This will
// allow the server to push udpates to the client
// using the internally-provided URL.
$.ajax({
type: "get",
url: "./ajax.cfm",
data: {
foo: "bar"
},
dataType: "json",
update: function( message ){
console.log( "Update: ", message );
},
success: function( response ){
console.log( "Success: ", response );
}
});
}
);
});
</script>
</head>
<body>
<h1>
Adding Pusher-Powered Update Support To jQuery AJAX
</h1>
<p>
<a id="doit" href="#">Do something really intense!</a>
</p>
</body>
</html>
As you can see above, my AJAX options include a new property, "update." This property allows the programmer to hook into an update event handler; in our case, I'm simply logging the update message to the console.
When I click the "Do something really intense" link, I trigger the AJAX request and get the following console output:
http://cf8.bennadel.com.......200 OK 5.8s
Update: Update [1] completed!
Update: Update [2] completed!
Update: Update [3] completed!
Update: Update [4] completed!
Update: Update [5] completed!
Success: success
As you can see, the update event handler was invoked five times before the success event handler was executed.
In order to do this, we need the server to push realtime notifications to the pusher service (which will, in turn, push them to the client). However, we don't want the server to have to know much about this process. To encapsulate this logic, I have created a jQuery plugin that augments the core ajax() method. When it sees that you are using the "update" event handler, this plugin constructs a "push" URL and appends it to the AJAX request parameters using the key, "ajaxUpdateUrl."
On the server side, the programmer simply needs to take this "ajaxUpdateUrl" URL and post a message to it using something like CFHTTP. By using this precompiled URL, neither the client nor the server really needs to know much about how the technology is wired together - it simply needs to provide the "update" event handler and make use of the given update URL.
Now that we've looked at the client, let's take a look at the ColdFusion script that handles the incoming AJAX request:
<!--- Param the URL variables. --->
<cfparam name="url.foo" type="string" />
<cfparam name="url.ajaxUpdateUrl" type="string" />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Now, we are going to simulate an AJAX request that is taking a
while to iterate over a number of complex functions. In reality,
we are just going to simuilate this with CFThread sleeping.
--->
<cfloop
index="i"
from="1"
to="5"
step="1">
<!---
For each iteration, we are going to push an update to the
client using the provided update URL. All we have to do is
append the update "message" to the given URL.
--->
<cfset update = "Update [#i#] completed!" />
<!--- Push update. --->
<cfhttp
method="get"
url="#urlDecode( url.ajaxUpdateUrl )##urlEncodedFormat( update )#"
/>
<!---
Sleep the thread for a second to simulate a very intenst
action taking place.
--->
<cfthread
action="sleep"
duration="1000"
/>
</cfloop>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Return our success response. --->
<cfcontent
type="application/json"
variable="#toBinary( toBase64( serializeJSON( 'success' ) ) )#"
/>
As you can see here, this ColdFusion page is expecting two variables: foo and ajaxUpdateUrl. The foo value is something the client provided explicitly; the ajaxUpdateUrl is the "push" url that our jQuery plugin has provided implicitly. When we need to push an update notification to the client, we simply pass our "message" to the ajaxUpdateUrl using CFHTTP.
In this case, we are using CFThread to simulate multiple, laborious server-side tasks. With each "task", we are sending an update message to the client. Then, when the AJAX request is complete, we simply respond with a "success" string.
So far, this has all been done without the client or the server having to know much of anything about the process; the realtime notification functionality has been fairly well encapsulated. Now, let's take a look at the jQuery plugin that helps make this possible. In the following code, our plugin is subscribing to a pre-defined Pusher Application. Each client (browser), gets a UUID that it uses as its subscription channel. Then, each ajax() request that makes use of "update", binds to a different event on that client-specific channel.
jQuery.ajaxPush.js
// Check to see if there is any UUID generator yet. We will need to
// try and create UUIDs for each client that opens this file.
//
// NOTE: UUID generator taken from:
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
if (!("createUUID" in window)){
window.createUUID = function(){
var S4 = function(){
return(
(((1 + Math.random()) * 0x10000) | 0)
.toString(16)
.substring(1)
);
};
// Return UUID-like value.
return(
(S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()).toUpperCase()
);
};
}
// We are going to augment the core AJAX functionaly. To do this, we
// are going to use Aspect oriented programming to wrap the old
// method with a wrapper that adds additional logic.
(function( $, oldAjaxMethod ){
// Set the pusher App key. All ajax instances will use the same
// application, but communiate over differnet keys.
var pusherKey = "e2e8b1634967c107dcbd";
// Create a "uuid" for this client.
var clientUUID = createUUID();
// Create an instance of the Pusher connection controller.
var pusher = new Pusher( pusherKey );
// This will be our actual channel subscription - there is some
// overhead to subscribing now; but, the upside is that it cuts
// down on possible delayed reactions later (it takes a few
// moments to actually make the subscription connection).
var pusherChannel = pusher.subscribe( clientUUID );
// Create an incrementing ID to keep track of the number of
// subscriptions. The UUID will be used to subsribe to a channel.
// The incrementing ID will be for the event type.
var eventTypeID = 1;
// Override the core jQuery AJAX functionality.
$.ajax = function( ajaxOptions ){
// Check to see if the "update" key is in options. If it is,
// then this AJAX request also wants to handle update events
// via PUSH.
if ("update" in ajaxOptions){
// Get a reference to "this" context so we can use it in
// the update event handler.
var self = this;
// Create a new event type for this channel.
var pusherEventType = ("update" + eventTypeID++);
// Bind to this event type. When the server pushes an
// update, this function will handle it.
pusherChannel.bind(
pusherEventType,
function( data ){
// Pass the push data off to the provided update
// event handler.
ajaxOptions.update.apply( self, arguments );
}
);
// Now that we have bound the update event handler, we
// need to add an update URL to the data collection so
// that the server will know HOW to push updates to the
// client.
//
// NOTE: This is just a proof-of-concept. In an actual
// app, I assume this would be hardcoded.
ajaxOptions.data = $.extend(
{},
ajaxOptions.data,
{
"ajaxUpdateUrl": escape(
window.location.href.replace(
new RegExp( "/[^/]+$", "i" ),
""
) +
"/push_ajax_update.cfm?channel=" + clientUUID +
"&event=" + pusherEventType +
"&message="
)
}
);
// Get a reference to the complete method so that we
// can unbind our channel listener.
var originalComplete = (ajaxOptions.complete || $.noop);
// Set our internal completel handler to unbind
// the channel event subscription.
ajaxOptions.complete = function(){
// There's no real way to unbind an event type, so
// let's just kill the update event handler in the
// the options.
ajaxOptions.update = $.noop;
// Call oritinal complete handler.
originalComplete.apply( self, arguments );
};
}
// At this point, we have updated the options in any way
// that we needed to. Let's now pass the control off to
// the original AJAX method.
return(
oldAjaxMethod.apply( this, arguments )
);
};
})( jQuery, jQuery.ajax );
At a high level, this plugin is intercepting the ajax() method so that it can augment the data collection and the complete event handler. If the client wants to use the "update" event handler, this plugin constructs a Push URL and appends it to the data collection. It then binds an event handler specific to this ajax request and hijacks the complete() event handler in order to "unbind" the update event specific to this ajax request once the request has completed.
Right now, this is just a proof of concept so there are values here that wouldn't need to be here in the long run. Ideally, I see this as some kind of hosted service that bakes in all of the required variables as part of the .js file request.
Jumping quickly back to the server-side, let me just show you what is in "push_ajax_update.cfm" - the target script that gets posted to the server as the ajaxUpdateUrl:
push_ajax_update.cfm (ajaxUpdateUrl)
<!--- Param the url values. --->
<cfparam name="url.channel" type="string" />
<cfparam name="url.event" type="string" />
<cfparam name="url.message" type="string" />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Create an instance of the Pusher with the given pusher values.
--->
<cfset pusher = createObject( "component", "Pusher" ).init(
"2129",
"e2e8b1634967c107dcbd",
"10b14f1234005a261164"
) />
<!--- Push update to client using PusherApp. --->
<cfset result = pusher.pushMessage(
url.channel,
url.event,
url.message
) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Return the status code. --->
<cfcontent
type="text/html"
variable="#toBinary( toBase64( result.statusCode ) )#"
/>
This file simply acts as a proxy to the Pusher service. The App Key in this file has to be the same as the key in the jQuery plugin; if this were a hosted plugin and proxy URL, this could all be easily encapsulated without any problems. And, of course, the Pusher.cfc being used here is my PusherApp ColdFusion component project.
Right now, this is just a proof of concept; but, I think it allows for a cleanly abstracted AJAX-based "update" event handler. Perhaps I can brainstorm with the Pusher service to see if something like this might be appealing to a larger audience.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben,
Great content. I'm really enjoying this pusher series. The coverage of javascript / jQuery is informational and fun. I've been a long time reader for the coldfusion stuff so its good to mix it up every now and again.
I look forward to you branching out even more. Moble, Service Side Javascript, who knows
Thanks
@Peter,
Thanks my man - realtime push notification is just a fun topic! And, what's nice is that services like Pusher allow us to push from ColdFusion to the client with relatively little effort.
Nice Ben.
Have you thought of trying with Nathan Mische Cf web socket gateway?
I got it running easily and just tried the demo chat app which worked fine across Chrome, FF and MSIE.
http://wiki.github.com/nmische/cf-websocket-gateway/
@Johans,
I have not tried any gateway stuff on my own just yet. Looks cool though, I'll have to find some time to check it out. Thanks for the heads up.