Smart Polling

I’m working on the rich UI of a small web application which has a problem I need to solve: the data from the server which the UI is presenting could change at any time. The first iteration of the UI has a refresh button. Clicking this button sends an Ajax request to a resource on the server which responds with a JSON data structure, and the UI is updated with any changes in the new data. My business partner doesn’t like the refresh button; he questions why it’s there and states how annoying it is to press the button all the time. His suggestion is the rich UI should smartly poll the server for changes to the data, and update the UI automatically.

Intrigued by his idea, we continued our discussion leading to a definition for what it means to smartly poll a server’s resource:

  • Use conditional GET requests
  • Retain the most recent Etag and Last-Modified date of the polled resource
  • Disable polling when the browser window is inactive

Implementing a smart polling process in our application’s rich UI gives us some desired benefits:

  • Removal of the refresh button
  • Automatic updating of the UI when the resource on the server has changed
  • Less repainting of the page since the DOM is touched only when the data has changed
  • Changes to the UI only happen when the window is active (the user sees them) as polling is paused while the user is doing something else

Creating a reusable component to achieve a smart polling process feels like the correct approach as I foresee a need to use this functionality in future projects as well.

Poller – YUI3 Component

Update: The IO Poller Module lives on the YUILibrary Gallery

I’ve moved the code to the YUI 3 Gallery on YUILibrary site; IO Poller on the YUILibrary Gallery.

The rest of this post is out-dated (comments are still good though)…

The remainder of this post pertains to Poller, a YUI3 compatible component hosted in the files sections of this site: http://925html.com/files/smart_polling/ (a Bazaar branch)

I’ve been developing several components and widgets using the new YUI3 component infrastructure; this is the first fully built and documented (including docs for the relevant YUI modules) one I’m releasing.

The Poller class is 3.3kb minified and 1.1kb gzipped, it requires io-base and base YUI3 modules.

The Poller class’ usage is through the public API methods: pause, sendRequest, start and stop; along with events: modified, request, response, start and stop.

Internally the Poller class is using conditional XHR GET requests. When polling is first kicked off (i.e. start() is called) the Etag and Last-Modified header values are cached. As subsequent polling requests are sent to the server the cached Etag is set for the If-None-Match HTTP request header, if no Etag was cached (i.e. the server doesn’t send Etags) then the cached Last-Modified date is set as the If-Modified-Since header. If a server doesn’t support conditional GET requests at all, sending neither an Etag nor a Last-Modified date, the Poller class will fire the modified event with every response.

Example instance of Poller

var myData = new Y.Poller({ url:'path/to/resource/', interval:5000 });
myData.on( 'modified', updateUI );
myData.start();

The above code will continue sending Ajax requests to the resource at path/to/resource/ until myData.stop() is called. If the Poller receives a 200 HTTP status (and not a 304 not modified status) the modified event will be fired passing the txId, response and args to the updateUI subscriber function. Subscribing to the poller:modified event is like subscribing to Y.io’s io:success event.

Disable polling on inactive windows

With the above functionality covering most of our requirements, we still need a way to pause polling when the browser window isn’t active. We came up with the idea to attach listeners to the window focus and blur events which call into protected methods to start/stop the polling process. Note: this functionality won’t interfere with the client code’s intent. i.e. if the client explicitly calls stop(), losing (blur) then gaining focus of the browser window won’t automatically start the polling process again. It respects the state of the Poller’s polling read-only attribute; calling start() will set the polling attribute to true and calling stop() will set the attribute to false.

To enable the functionality to pause and resume polling on the window’s focus and blur events: either set the pauseInactive attribute, or declare pauseInactive: true on the config object during construction.

Using the myData Poller instance from above:

myData.set( 'pauseInactive', true );

Setting the paseInactive attribute on construction:

var myOtherData = new Y.Poller({
	url : 'path/to/resource/',
	interval : 5000,
	pauseInactive : true
});

Putting it all together

I have a small demo for this project which continually checks a resource on the server to see if it has changed and updates the page if it has. The resource is a super simple JSON object with two properties: label and time, where time is a timestamp indicating the time on the server when the file was last modified.

{ "label":"the server's time was", "time":1238530883810 }

There is a button on the page which will update the data file stamping the new time on the server as the value of the time property. You can press this button to update the data file and after the next polling request is sent the UI will be updated to reflect the change in the file. I’ve included a YUI Console on the page which I send log messages to so you can see what’s going on.

The following is the code pertaining to the Poller instance used in the demo:

var poller = new Y.Poller({
	url : 'data.json',
	headers : {"foo":"bar"},
	interval : 7000,
	pauseInactive : true
});
poller.on( 'request', logRequestData );
poller.on( 'response', logResponseData );
poller.on( 'modified', updateUI );
poller.start();

Run The Demo

Note: I’ve have some issues running the demo on IE8 and IE7, it has something to do with the YUI Console; enabling the Developer Tools in IE8 made the demo work correctly.

If you liked this post you can suppot me with a Tip

10 Other Comments

22 Responses to “Smart Polling”


  • Have you thought about taking a comet approach to this rather than ajax requests on some set intervals? This would allow for a much more accurate representation of the server’s state (as the server would ‘push’ data down rather than being polled) and would also get rid of edge cases like not polling if the user is focused on the address bar (which fires the onblur event on the window, at least in browsers like Firefox). As for the YUI console not working in IE it may have something to do with YUI expecting the console object to already exist (I’m not 100% sure though).

  • @illvm We defiantly thought about using a comet approach to solve our problem as it has great advantages like you’ve mentioned. Our decision to choose a polling Ajax strategy was also impacted by it’s implementation simplicity; no fancy server-side code needed, no separate comet client-side library involved doing it’s crazy cross-browser hacks. Servers are great at receiving then handling requests fast (especially conditional GET requests), standard life-cycle XHRs works great in all modern browsers. We are aware that not being smart with our polling requests would increase server load, and updating the UI with every XHR returned response could cause the browser to re-paint from the DOM touches. These are undesirable side-effects of polling which we’ve hopefully mitigated here.

    Thanks for the input about Firefox firing the window:blur event when clicking in the address bar; that’s good to know. The pausing of the polling process when the window has gone “inactive” is the part I struggled with the most; this can be improved upon. My goal was to make sure it didn’t have any ill-effects on the polling process in the modern browsers.

    I’ve also came the same conclusion as you with respect to the demo’s YUI Console and IE. I planned to flush out the issue more before reporting it to the YUI team.

  • @Eric According to IE8 the offending code for the console is in YUI starting around line 631:

    if (!bail) {
    
       if (c.useBrowserConsole) {
          var m = (src) ? src + ': ' + msg : msg;
          if (typeof console != 'undefined') {
            var f = (cat && console[cat]) ? cat : 'log';
            console[f](m); /*<--what IE8 is finding offensive */
          } else if (typeof opera != 'undefined') {
            opera.postError(m);
          }
       }
     //. . .
    }

    What’s weird is that in IE8 (not sure about IE7) if you check the type of console it returns as an object instead of undefined. But this object does not have the log, info, or warn methods until after dev tools have been loaded. I’m not really sure why YUI thinks it should have a console or why the console is being partially created as if you got a page that doesn’t have YUI or any other sort of console the console variable will be undefined instead of an object.

    Either way it seems like a bug in YUI because they are making the assumption that at the very least the console object will have a ‘log’ property and then trying to call that property as a function without even checking if it is indeed a function (ditto for all other ‘cat’s). This seems like a rather big assumption since as far as I know there is no standard definition for browser consoles.

  • @illvm Good find! Thanks for doing the hunting and finding the culprit section of code. I’ll post an issue on the YUI3 Issue Tracker and reference your find here. Although it is odd that IE8 sets console to an object when it’s undefined.

  • Wondering… aren’t conditional requests supposed to be handled automatically by the browser? Have you conducted tests whose results show that browsers don’t do it for you for free?

  • @Thomas That was my impression when first looking into this. What I found was the browser didn’t even seem to ping the server at all for a cached XHR to the same resource! So in my example when you’d click the button to update the data file on the server, the UI would never update, because the browser was returning it’s cached representation of the entity (which was also stale). This was true for a server which was returning Etag and Last-Modified response headers. I noticed this behavior on Firefox 3.0.8; Safari 4 was at least updating the UI, but each time the Ajax polling process sent off an XHR a 200 OK response status was returned with the full entity body.

    I wanted to ensure that a standard conditional GET XHR was being preformed consistently across modern browsers; so when setting up each XHR I made sure to add the appropriate request headers. I didn’t want to rely on what browsers thought they should do internally and what YUI’s default XHR was doing because of the inconsistencies I was noticing.

  • hooray for conditional get… too bad most people dont even know what that is yet, but its very forward thinking.

    This comment was originally posted on http://ajaxian.com/)“>Ajaxian

  • When opening the example in a new tab in the background you could wait for window.onfocus to start polling.

  • What about not pausing the updates, but just incrementing the interval on blur? For example it would be useful if the application would like to alert the user by a sound if something have changed, or in the case the focus/blur detection fails.

  • @András and @Kristof I made the automatic-pausing a configuration attribute, pauseInactive, which defaults to false.

    Kristof, your suggestion could easily be implemented in the client code by wrapping the call to the start() method with an event listener:

    Y.on( 'focus', poller.start, 'window' );

    András, if the pauseInactive attribute is set to false (which is the default), you could then implement your suggestion as follows:

    var soundHandle = null,
    	playSound = function(){...};
    
    Y.on('blur', function(e){
    	poller.set('interval', 14000);
    	soundHandle = poller.on('modified', playSound);
    }, 'window');
    
    Y.on('focus', function(e){
    	poller.set('interval', 7000);
    	soundHandle.detach();
    }, 'window');
  • Have you thought about using the Cache-Control max-age header, when returned from the server, to figure out how long to wait before polling next? In the absence of the explicit interval option, it could be used to know how long to cache the response for. In the case where no Cache-Control max-age was returned and no interval was specified, the system could just fall back to it’s current behavior and poll every 10,000ms.

  • @Eric Ferraiuolo

    Typo: poller.set(‘inertval’, 14000);

  • @Dan I like your thinking here. This would effectively allow the server to tell the client when to ping back to see if there has been an update to the resource. I’m unsure how common this type of behavior would be used by either the client developer to let the sever tell what to do, or by a server developer to have an accurate prediction of when dynamic data would change. It seems, in practice, that max-age Cache-Control values are on the magnitude of hours or greater. That being said, I feel your suggestion falls into the smart category here; effectively making the component know how to listen to the server in yet another way. I’ve started looking into how to implement this functionality and will try to include it in the next rendition of this component. My caution is to make sure it’s clear to the client API that checking of the max-age Cache-Control value might be going on in the background if no interval configuration value is set. Currently it is clear to the client that if they don’t set an interval value that 10,000ms (10 seconds) will be used.

    @Chris thanks for the heads-up, fixed the typo.

  • I’m trying to get this working, and the polling seems to be doing what it’s supposed to, but I haven’t been able to make it detect that a change has been made.

    The data.php file isn’t downloadable (comes down as a blank page), so I’ve been simply changing the timestamp in the data.json file by hand. however, it doesn’t seem to be noticing that the file has been updated.

    So either it checks something other than the timestamp, or I’m completely missing something important by not seeing the php file.

    Would you mind posting the contents for ajax newbies like me?

  • well, never mind, for some reason the data.json file wasn’t uploading using dreamweaver.

    I logged into the server and changed it with notepad manually and it did update.

    Now I just have to make it read something besides timestamp.

    Thanks for the cool API

  • @Andy The code will preform conditional GET requests to the resource via: If-None-Match (if it has a cached Etag vale) or If-Modified-Since (if it has a cached LastModified date), in that order; Etag takes priority over LastModified. In the case where responses from the resource do not provide either a LastModified or Etag, the code will assume it’s changed, firing the Modified event (useful for polling dynamic resources).

  • Hmm, so I haven’t used Etag values before, but I tried to set it with the PHP header method:

    include("include/database.php");
    
    $q = "Select * From qryLatestPost";
    $result = $database->query($q);
    $num_rows = mssql_num_rows($result);
    
    if($num_rows > 0){
    	$DatePosted = mssql_result($result,0,"DatePosted");
    	$DisplayName = mssql_result($result,0,"DisplayName");
    	$PageText = mssql_result($result,0,"PageText");
    	$PageText = addslashes($PageText);
    
    	// create ETAG (hash of feed)
    	// (HTTP_IF_NONE_MATCH has quotes around it)
    	$eTag = md5($DisplayName.$DatePosted);
    	header('Etag: '.$eTag);
    
    	$text = "{ \"label\":\"$DisplayName\", \"time\":\"$DatePosted\" }";
    
    	// compare Etag to what we got
    	if ($eTag == $_SESSION["HTTP_IF_NONE_MATCH"]) {
    		header("HTTP/1.0 304 Not Modified");
    		header('Content-Length: 0');
    	} else {
    	// dump feed
    		echo $text;
    	}
    	$_SESSION["HTTP_IF_NONE_MATCH"] = $eTag;
    }

    It is technically a dynamic source, but if there hasn’t been a new post, it doesn’t need to update every 5 seconds.

    If there is a better way to do this, I’d love to hear about it, otherwise I’ll just keep researching Etags.

  • @Andy, I’m not a PHP guy; so I’m unsure about your code you posted. May I ask what your goals are here; high-level what are you hoping to accomplish?

  • Your concept is awesome, thanks! It’s given me inspiration for a polling JS component I’m making.

  • At sports.yahoo.com, we let the browser do conditional gets and with a proper Max-Age header emitted by the server, all modern browsers properly make the request at the frequency we specify.

    IE 6 has some gotcha’s so we’re planning on using a random variable to bust through caches on that browser, but everywhere else, just relying on the browser to do the right thing has worked well.

    See implementation here (when games are going on):
    http://rivals.yahoo.com/ncaa/football/scoreboard

    So I question the need for this library.

    JS here: http://l.yimg.com/j/static/versioned_asset/v7/js/editorial/js/scorestream.js

  • And I also agree that it’s great to promote this standards based way of refreshing content on the page. E-tags and conditional gets are quite powerful and can solve many use cases with off the shelf software.

  • @EJ I have since over the months reduced the features of this utility, and the newest code is up on GitHub: http://github.com/ericf/yui3-gallery/tree/master/build/gallery-io-poller/

    My comment above explains the reason for using the Last-Modified date and Etag headers and making Conditional GET requests.

    There’s a fundamental difference in your approach using Max-Age, and my approach using Conditional GET requests. I want to check if there’s a change on the server and know when that change happens ASAP. I have no way to know when the change is going to happen on the server, therefore have no way to accurately set the Max-Age header.

    I see what you’re saying about the modern browsers respecting Max-Age and evicting their cached XHR-requested resource representations; this is the exact problem I had, browsers are good at caching the responses of XHR requests.

    My question for you is: How do you know what to set the Max-Age response header to? How can you predict when something like a sports score will change?

    I want to be in control of how the browsers are caching all resource representations on my applications (static UI files, XHR-requested resources, etc.); this was the motivating factor behind sending over Conditional GET requests on each XHR-poll of a resource. The server should be quick at determining if what the client has cached is stale or not by comparing an Etag; saving the hit of generating the entity body and sending the payload over the wire by simply responding with a 304 Not Modified response.

Comments are currently closed.

Additional comments powered by BackType