Today on Blogcritics
Home » Culture and Society » Science and Technology » Easy Ajax: A Case Study

Easy Ajax: A Case Study

Please Share...Tweet about this on Twitter0Share on Facebook0Share on Google+0Share on LinkedIn0Pin on Pinterest0Share on TumblrShare on StumbleUpon0Share on Reddit0Email this to someone

Developing web applications using “Ajax”, or Asynchronous Javascript And XML, is easy. So easy that it is easy to overlook “gotchas” and make major mistakes, or to try to use Ajax in situations for which it is inappropriate. This article outlines a successful use of Ajax to add concurrency control to a blogging system, and the issues I encountered during development.

When To Use Ajax

AJAX is a web development technique for creating interactive web applications. Ajax applications are mostly executed on the user’s computer; they can perform a number of tasks without their performance being limited by the network. This permits the development of interactive applications, in particular reactive and rich graphic user interfaces. Jesse James Garrett of Adaptive Path is generally credited with popularizing the term “Ajax” as a result of his seminal essay on the technology.That article introduces the concepts and establishes that the Ajax approach can work well, but it does tend to leave people with the impression that Ajax can solve every problem, and that just isn’t the case.

The biggest problem with applications that use Ajax is that the URL never changes. In Google Maps, for example, there is a “Link to this page” function to get around this problem. It takes careful planning to know when to use Ajax and when to just link to a new URL.

How To Use Ajax

The problem facing me happened to involve a heavily-customized version of a popular blogging system, but only three very minor bits actually have anything to do with that. More importantly, I needed to implement a concurrency control system so that multiple editors couldn’t edit the same article and overwrite changes from each other.

In this case study, I want to ensure that only one person at a time has the right to edit a given article. I could issue a database call at page-load to see if anybody else has it locked, or to lock it myself, but how would I know when a user is done editing the page? If the user navigates away from the page, how can I let the database know? I can’t refresh the entire page at a regular interval, because editing changes would be lost. I need to be able to constantly let the server know that the user is still actively editing an article, and when that regular update stops, then I’ll know that the user has moved on. The only URL will be an internal URL which never needs to be bookmarked, so there are no worries there!

I used a bit of javascript and a separate internal web page written in PHP. That’s it. In this case, I didn’t even use any XML.

Javascript


var req;

function initPage() {

  kaTimer=setInterval(keepAlive, 1000);

}

function keepAlive() {

  var url = "/mt/keepalive.php?aid=<TMPL_VAR _

                  NAME=AUTHOR_ID>&eid=<TMPL_VAR NAME=ID>";

  if (window.XMLHttpRequest) {

    req = new XMLHttpRequest();

    req.onreadystatechange = processReqChange;

    req.open("GET", url, true);

    req.send(null);

  } else if (window.ActiveXObject) {

    req = new ActiveXObject("Microsoft.XMLHTTP");

    if(req){

      req.onreadystatechange = processReqChange;

      req.open("GET", url, true);

      req.send();

    }

  }

}

function processReqChange() {

  if(req.readyState==4){

    if(req.status==200){

      if(req.responseText=='OK'){

        document.getElementById('saveButton').style.visibility='visible';

        document.getElementById('ajaxConsole').innerHTML='';

        clearInterval(kaTimer);

        kaTimer=setInterval(keepAlive, 30000);

      }else{

        document.getElementById('ajaxConsole').innerHTML = 'LOCKED: ' + _

                  req.responseText;

        if(document.getElementById('ajaxConsole').innerHTML.substr(0,8) _

                  == 'LOCKED: ') {

          clearInterval(kaTimer);

        }

      }

    }else{

      alert('Problem: [' + req.status + '] ' + req.statusText);

      clearInterval(kaTimer);

    }

    req.onreadystatechange = new function(){};

  }

}

function activateEditing() {

  document.getElementById('saveButton').style.visibility='visible';

  document.getElementById('ajaxConsole').innerHTML='';

  clearInterval(kaTimer);

  kaTimer=setInterval(keepAlive, 30000);

}

window.onload=initPage();

With this code, I’ve set up three functions: initPage(), keepAlive(), and processReqChange(). Your own Ajax code will need functions very much like these. The window.onload=initPage(); starts the ball rolling.

First, let me paint a little picture of what the rest of this page looks like. It’s an article-editing screen, with buttons to Save, Preview, and Publish. The ID for the Save button is “saveButton”. Near the top of the screen I’ve added a div that looks like this:

<div id="ajaxConsole"><TMPL_UNLESS NAME="STATUS_DRAFT">Do not edit this article! Wait until this text either disappears or is replaced by a "locked" message.<br/>It shouldn't take more than a couple of seconds. If you do not have Javascript enabled, you will not be able to edit an article in 'Pending' or 'Publish' status.</TMPL_UNLESS></div>

The TMPL tags are interpreted by the publishing system so that this div doesn’t exist when an article has never been saved. Clearly there can be only one author then!

When the page is first loading, that text will be visible. Once the window.onload event is triggered, however, the initPage() function should be called, which tells the browser to call the keepAlive function once per second.

Warning! The window.onload can be triggered before the page has actually finished loading, which can cause… oddness. It is for this reason that I set the function to be called at frequent intervals, so it will seem to complete as soon as the page has completed loading, rather than waiting until the next interval comes around.

The keepAlive function calls the PHP page I’ll describe later. For now, understand that any URL could be used, and this one should return either “OK” or something else. The TMPL_VARs are again supplied by the publishing system to track the current editor and article.

The reason for the if is that even though Microsoft created XMLHttpRequest, Internet Explorer doesn’t actually support the function without the use of ActiveX. Firefox, Safari, and other browsers do support it, however, and so we first check to see if one of those will work, and only fall back on the ActiveX approach if that fails. In either case, the idea is the same: we instantiate the object, tell the browser to call the processReqChange function if anything happens, and then open the URL and send it a null to get it moving.

This is where the asynchronous part comes in. The javascript function ends and the browser goes along its merry way. If the XMLHttpRequest object never returns, the browser is not hung up. When the object’s state does change, the processReqChange function is fired off.

There are several different “ready states” the object can be in, so we first check to make sure we’re in readyState 4, meaning we’re finished. Given that, and given that we have control over the server side, the most likely result is that our status is 200, the standard HTTP status for “success.” If that isn’t the case, something unusual must be happening, so we’ll display some error information and clear the interval so we don’t keep hammering the server.

Most likely, though, is that our HTTP GET has finished successfully and returned with “OK” or something else. If the result is “OK”, we know that we’ve been granted exclusive access to this article, so we can clear the warning message, enable the Save button, and set the keepAlive interval to a more reasonable rate to ensure that we maintain our exclusive access to this article.

Warning! You might think that using the same object name with a different interval value would reset the existing interval, but in Internet Explorer at least, that isn’t the case. The intervals are cumulative, so you must first clear the existing interval before setting a new one.

If the result from the GET is anything other than “OK”, that means (because of the PHP code we’ll get to in just a few paragraphs) that this article is locked by someone else. In that case, we’ll set the contents of that div to explain that the article is locked and clear the interval so we don’t bother even checking any more. After all, by the time this article isn’t locked anymore, the contents will likely have changed, so we want the user to refresh the page. We could actually refresh the contents automatically, and we probably will in a future version of this code.

Browser coding in general, and Ajax specifically, is fraught with timing issues, and this is one case in which I added some code to allow for them. After setting the contents of the div with the error message, I then check to make sure that the div’s contents are actually what I just them to be. On rare occasions, such as when the page hasn’t actually finished loading, they aren’t! In that case, I just let the cycle continue, hoping things will work the next time through.

Warning! The next line of code is quite important, and before adding it I had people complaining that their entire computer systems were hanging up while editing articles. I couldn’t reproduce the problem, but after digging in, I finally figured it out. Internet Explorer doesn’t release the memory used by these “req” ActiveX objects, instead letting them pile up, 4K or so at a time. Eventually, Internet Explorer is using so much memory that it hangs, and when Internet Explorer hangs, problems for your Windows system are sure to follow. Bizarrely, you can’t just set the event value to null, but must set it to an empty function! With req.onreadystatechange = new function(){}; in place, Internet Explorer manages memory a bit more reasonably, without any growth over time.

PHP


<? require('../includes/connect.php');

$interval=130; # Seconds

$entry_id=$_REQUEST['eid'];

$editor_id=$_REQUEST['aid'];

$checkq="SELECT workflow_created_by,workflow_created_on,

      NOW()-INTERVAL $interval SECOND,workflow_closed_on,author_name,NOW()

      FROM mt_workflow,mt_author WHERE workflow_created_by=author_id

      AND workflow_entry_id=$entry_id AND workflow_type=1";

$clearq="DELETE FROM mt_workflow WHERE workflow_entry_id=$entry_id

        AND workflow_type=1";

$reserveq="INSERT INTO mt_workflow VALUES (0,$entry_id,$editor_id,1,NULL,

      $editor_id,NOW(),NULL,NOW())";

$refreshq="UPDATE mt_workflow SET workflow_closed_on=NOW(),

      workflow_created_by=$editor_id WHERE workflow_entry_id=$entry_id

      AND workflow_type=1";

$rslt=mysql_fetch_row(mysql_query($checkq));

if($rslt[0]){

  if($rslt[0]==$editor_id){

    # Me!

    $rslt=mysql_query($refreshq) or die(mysql_error());

    echo 'OK';

    exit;

  }

if(Date("Y-m-d-H-i",strtotime($rslt[3]))>=Date("Y-m-d-H-i",strtotime($rslt[2]))){

    # Locked

    echo 'This article has been locked by '.$rslt[4].' since '.$rslt[1].'.<br/>

        (Server time is now '.$rslt[5].', and '.$rslt[4].

        ' was last seen at '.$rslt[3].'. Any contact within the last '.

        $interval.' seconds is considered current.)<br/>

        In order to ensure that editing changes are not lost, you will need to

        refresh this page in order to edit this article. And, of course, '.$rslt[4].

        ' will need to be done with it!';

  }else{

    # Old, so me!

    $rslt=mysql_query($clearq) or die(mysql_error());

    $rslt=mysql_query($reserveq) or die(mysql_error());

    echo 'OK';

  }

}else{

  # Nothing, so me!

  $rslt=mysql_query($reserveq) or die(mysql_error());

  echo 'OK';

}

?>

This is the code called as /mt/keepalive.php from the javascript. After ensuring that I can get to the mysql database and gathering my variable, I build a few queries, not knowing yet which ones I’ll need. I then execute one, which is this:

SELECT workflow_created_by,workflow_created_on,NOW()-INTERVAL $interval SECOND,workflow_closed_on,author_name,NOW() FROM mt_workflow,mt_author WHERE workflow_created_by=author_id AND workflow_entry_id=$entry_id AND workflow_type=1

The table ‘mt_workflow’ is something I created, and tracks a couple of other things in addition to editing concurrency; hency workflow_type. In this case, I should end up with information about the last person seen editing the article, if any.

If there is no record matching this article ID, we take the outer loop, labeled “Nothing, so me!” There we reserve the slot for the user:

INSERT INTO mt_workflow VALUES (0,$entry_id,$editor_id,1,NULL,$editor_id,NOW(),NULL,NOW())

If there is a record, it might be the user’s own from a previous call to this page, possibly 30 seconds earlier. If so, we take the first clause, labeled “Me!”, and update the timestamp to note that we still have the article open:

UPDATE mt_workflow SET workflow_closed_on=NOW(),workflow_created_by=$editor_id WHERE workflow_entry_id=$entry_id AND workflow_type=1

If a record does exist for someone else, I might still be okay. If it’s an old record — defined in this case as 130 seconds old, but rounded to the minute — then I take the “Old, so me!” clause. The old user has apparently navigated away from the window or closed their browser, so I clear out the old record and create one for the current user instead:

DELETE FROM mt_workflow WHERE workflow_entry_id=$entry_id AND workflow_type=1

INSERT INTO mt_workflow VALUES (0,$entry_id,$editor_id,1,NULL,$editor_id,NOW(),NULL,NOW())

Everything Else

There is nothing else. Ajax programming is as simple as setting up a Javascript timer, creating an XMLHttpRequest, object, and handling the result when that object returns. As long as you think about timing issues and keep an eye on memory usage, you’re now well on your way.

Bad Code

The sharp-eyed observer will note that if it take longer than one second for keepalive.php to return, things could get pretty busy. Fortunately in this case, it’s a very fast server. I’ve considered setting a flag when I open the object and clearing it when the object returns, and ignoring the flag if several seconds have passed, but so far it hasn’t caused any problems, and I’d rather avoid the added complexity.

Powered by

About pwinn

  • http://selfaudit.blogspot.com Aaman

    I wish that Ajax book was cheaper:)

    Incidentally, is this true AJAX, does the reliance on PHP and SQL not mean a new paradigm, not reliant on XML?

  • http://w6daily.winn.com/ Phillip Winn

    It’s still XMLHttpRequest, so all three elements of AJAX are present. The PHP and Mysql are, I think, incidental. It could as easily be anything else, or an external webservice, on the “back end.”