Halhelms
SIGN UP FOR MY NEWSLETTER
 
 
Halhelms

Shameless Money

Recent Entries

RSS

Subscribe

Learning jQuery, Day 10: Keeping Queues in Sync

I've been working on a job where different people in different places are working off of a shared queue of work orders to be processed. When they first log onto the system, they see the current work orders and their status in a table. (Don't tell the CSS police that I used the "T" word...) The problem is that, as existing work orders are processed and new work orders are submitted, their screens quickly are out of sync.

This leads to serious problems, including multiple people processing (or at least attempting to process) the same work order. After some thinking about this, I decided to refresh the queue whenever a significant event occurred. I defined a significant event as one of the following:

  • A new work order was submitted
  • Processing on a work order had begun
  • Processing on a work order had completed

I knew that since I didn't have a pipe between client and server, I would have to do some polling, but I didn't want to just refresh the queue every n seconds. Instead, I decided to ping the server every n seconds to find out if anything had changed. If it had, then I would refresh the queue.

You can see the results of this code at http://dev.citymind.com:8500/test/blog_jquery/day10/index.cfm. You'll see some tests I ran. If you enter a work order, the queue will refresh. But to make this do what I want, it's important that if anyone makes a change, your queue should refresh. Let's look at one way to accomplish this.

To simulate both the screens of both the work order processor and the contractor submitting work orders, I created a combo screen of pure HTML, index.cfm:

<html>
<head>
</head>
<body>
<table>
   <tr>
      <th>You are the Processor</th>
      <th>You are the Contractor</th>
   </tr>
   <tr>
      <td id="queue">
      </td>
      
      <td>
         <form id="contractor_form">
            <label for="wo_number">Work Order Number: </label>
            <input type="text" id="wo_number" name="wo_number"><br />
            <label for="wo_amount">Work Order Amount: </label>
            <input type="text" id="wo_amount" name="wo_amount"><br />
            <input type="submit" value="Submit Work Order as Complete">
         </form>
      </td>
   </tr>
</table>

<cfinclude template="index.pgm">
</body>
</html>

Well, it's almost pure HTML; I do have an include file: index.pgm. I've blogged before about this, so I'll be very brief in saying that for some time, I've taken to separating markup from code by splitting them into two separate files. This has proven to be enormously helpful.

The contents of index.pgm are:

<cfoutput>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="jquery.polling.js"></script>      

<script type="text/javascript">
   var myTimeStamp = #application.queueTimeStamp#;
   
   function refreshQueue(){
      $.get(
         'Processor.cfc?method=workOrderQueue',
         {},
         function(response){
            $('##queue').html(response.display);
         },
         'json'
      );
      $('##notificaton').empty();
   };
   

      
   $.poll(
      {
         url: "Processor.cfc?method=getQueueTimeStamp",
         type: "GET",
         interval: 4000,
         success: function(response){
            if (response.queueTimeStamp != myTimeStamp) {
               refreshQueue();
            };
            myTimeStamp = response.queueTimeStamp;
         }
      }
   );
   
   $('##contractor_form').submit(function(){
      $.post(
         'Processor.cfc?method=complete',
         {
            wo_number : $('##wo_number').val(),
            wo_amount : $('##wo_amount').val()
         },
      Çspan style='color: #808080'ÈÇemÈ   // callback
Ç/emÈÇ/spanÈ         function(){
            $('##wo_number').val('');
            $('##wo_amount').val('');
         }
      );
      return false;
   });
   
   refreshQueue();

</script>
</cfoutput>

I begin by including jQuery from the Google repository. Next, I include a jQuery plugin that I wrote based on a polling script at buntin.org. The purpose of this is to periodically "ping" the server and do something in response.

jQuery.polling.js:

// based on Seth Buntin's code at buntin.org
jQuery.poll = function(options){
Çspan style='color: #808080'ÈÇemÈ   // default options
Ç/emÈÇ/spanÈ   var defaults = {
      type: "POST",
      url: ".",
      success: '',
      interval: 2000,
      dataType: 'json'
   };
   
   var opts = jQuery.extend(defaults, options);
   
   setInterval(update, opts.interval);

Çspan style='color: #808080'ÈÇemÈ // method used to update
Ç/emÈÇ/spanÈ   function update(){
      $.ajax({
         type: opts.type,
         url: opts.url,
         success: opts.success,
         dataType: opts.dataType
      });
   };


   return this;
};

The purpose of including this code is to allow me to easily specify a polling strategy. I'll explain how I'm using this code shortly. For now, you can treat it as a black box that, when called with the appropriate options, will ping the server at specified intervals.

Back to index.pgm and this code:

var myTimeStamp = #application.queueTimeStamp#;

When the page is first loaded, I place a timestamp in a JavaScript varible, "myTimeStamp". I'm going to use this to check against a "queueTimeStamp" variable returned by my polling code. IF the timestamp is different, then a significant event has occurred and has updated the timestamp. And, if that happens, I'll refresh the queue. Since the timestamp being returned by the polling code is in the application scope, a significant event caused by anyone will cause the timestamp to be altered and my page will refresh. Just what I want to happen.

Let's go over the rest of the code.

The "refreshQueue" function:

function refreshQueue(){
      $.get(
         'Processor.cfc?method=workOrderQueue',
         {},
         function(response){
            $('##queue').html(response.display);
         },
         'json'
      );
      $('##notificaton').empty();
   };

The "refreshQueue" function will be called if the two timestamps (mine and the server's) differ. It's a "GET" request performed through Ajax to a Processor.cfc that serves up the work order queue. This method will return a snippet of HTML code that will be inserted into the appropriate section of the HTML and leave the page itself unrefreshed.

The "$.poll" call:

$.poll(
      {
         url: "Processor.cfc?method=getQueueTimeStamp",
         type: "GET",
         interval: 4000,
         success: function(response){
            if (response.queueTimeStamp != myTimeStamp) {
            Çspan style='color: #808080'ÈÇemÈ   //$('##notification').html('Your queue needs to be refreshed.<br><input type="button" id="refresh" value="Refresh Queue" onclick="javascript:refreshQueue();">');
Ç/emÈÇ/spanÈ               refreshQueue();
            };
            myTimeStamp = response.queueTimeStamp;
         }
      }
   );

Having registered "jQuery.polling.js" with the jQuery library by including the file, I can call "$.poll" and provide it with options that will override the default ones. When "Processor.cfc" is called with a method of "getQueueTimeStamp", it returns a JSON object with a property, "queueTimeStamp". I check this against "myTimeStamp". If the two are different, I call "refreshQueue" to get the latest version of the queue.

Trapping the contractor form submission:

$('##contractor_form').submit(function(){
      $.post(
         'Processor.cfc?method=complete',
         {
            wo_number : $('##wo_number').val(),
            wo_amount : $('##wo_amount').val()
         },
         function(){
            $('##wo_number').val('');
            $('##wo_amount').val('');
         }
      );
      return false;
   });

When the contractor form is submitted, I trap the submission and pass the values of the form fields to "Processor.cfc", calling its "complete" method. (That "complete" method updates application.queueTimeStamp.) It then clears the values typed in so that the form is ready for another submission.

The final bit of code is a call to get things started out by calling "refreshQueue" to see the current state of the queue.

The "Processor.cfc" code:

<cfcomponent displayname="Processor" output="false">
<cfsetting showdebugoutput="false">   
<cffunction name="complete" access="remote" output="false">
   <cfargument name="wo_number" required="true">
   <cfargument name="wo_amount" required="true">
   <cfquery datasource="learnjquery">
      INSERT INTO workorders(
         workOrderId,
         workOrderAmount,
         dateSubmitted
      ) VALUES(
         <cfqueryparam cfsqltype="cf_sql_varchar" value="#wo_number#">,
         <cfqueryparam cfsqltype="cf_sql_money" value="#wo_amount#">,
         <cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#">
      )
   </cfquery>
   
   <!--- update queueTimeStamp --->
   <cfset application.queueTimeStamp = Val(Now() * 60000)>
   <cfreturn>
</cffunction>



<cffunction name="getQueueTimeStamp" access="remote" output="false" returnformat="json">
   <cfset var response = StructNew()>
   <cfset response = StructNew()>
   <cfset response['queueTimeStamp'] = application.queueTimeStamp>
   <cfreturn response>
</cffunction>


   
<cffunction name="workOrderQueue" access="remote" output="true" returnformat="json">
   <cfset var WorkOrders = "">
   <cfset var response = StructNew()>
   <cfset response['display'] = "">
   
   <cfquery datasource="learnjquery" name="WorkOrders">
      SELECT * FROM workorders ORDER BY dateSubmitted DESC
   </cfquery>
   
   <cfsavecontent variable="response.display">
      <div id="notification"></div>
      <table id="workorders">
         <tr>
            <th>No</th>
            <th>Amount</th>
            <th>Date Submitted</th>
         </tr>
      <cfloop query="WorkOrders">
         <tr>
            <td class="work_order" title="#workOrderId#">#workOrderId#</td>
            <td>#DollarFormat(workOrderAmount)#</td>
            <td>#TimeFormat(dateSubmitted, 'hh.mm.ss')#</td>
         </tr>
      </cfloop>
      </table>   
   </cfsavecontent>
   <cfreturn response>
</cffunction>

</cfcomponent>

So, with a bit of jQuery, we're able to have a mechanism to refresh a page without incurring the costs of refreshing pages that haven't changed.

Zipped files

Comments
Sung Wa's Gravatar What are the special characters in jquery.Polling.js ?
# Posted By Sung Wa | 5/7/09 2:39 AM
Hal's Gravatar @Sung Wa -- Those aren't special characters; it's just the way the blog interprets double slashes (I think)
# Posted By Hal | 5/7/09 11:31 AM
John Quarto-vonTivadar's Gravatar So it's possible that if I'm looking at one subset of the data, and you're looking at another (diff't) subset of the data, then you could trigger an event that causes a refresh even though it doesn't affect my data set?

example: I'm looking at Customers from California, and you're looking at Customers from Missouri, and you update one of those Missourans and then my California list would get refreshed. What happens if i'm in the middle of an edit when the refresh occurs?
# Posted By John Quarto-vonTivadar | 5/7/09 9:26 PM
Hal's Gravatar @John
On your first question, remember that this is code for teaching purposes only. In reality, you wouldn't have a single timeStamp, but multiple ones based on events being affected.

As for the "what if I'm editing?", in the app we just finished, we just put a button for "Refresh" and gave the user a message: Your queue needs to be refreshed.
# Posted By Hal | 5/8/09 9:26 AM
 
   
Clicky Web Analytics