-
Jonathan Hedstrom
Lead Developer
I am writing an application where I have a block whose data is coming from a 3rd-party service. Since querying that service can take some time, I am caching the data locally, so pages load very quickly. However, in the interest of keeping the data more up-to-date than the hourly updates that happen during cron runs, I had the idea to have the client browser periodically poll for updated data. Fortunately javascript (and Drupal's jQuery implementation) makes it fairly easy to set up this periodic polling from the client side.
Disclaimer: the following code-snippets were taken directly from the application, but have been modified since the actual module names won't make much sense here.
The first step is to convey some parameters to the browser via Drupal.settings:
$settings = array( // The initial timeout, in milliseconds, to pass to setTimeout(). 'timeout' => $timeout, // A menu callback that will return JSON containing updated data. 'callback_url' => $callback, // The re-occuring time for periodic polling, after the initial $timeout period has passed. 'cache_lifetime' => $cache_lifetime * 1000, // The node ID, used for CSS ID selectors. 'nid' => $data->nid, ); drupal_add_js(array('mymodule' => $settings), 'setting');
Secondly, I add the corresponding javascript file that contains the code:
drupal_add_js(drupal_get_path('module', 'mymodule') . '/mymodule.js');
// This variable can be used to terminate the timeout via // clearTimeout, or in this case, it is used so that repeated timeouts // aren't set by additional calls to Drupal.attachBehaviors. var myModuleTimeout = 0; Drupal.behaviors.myModuleFunction = function (context) { if (Drupal.settings.mymodule && !myModuleTimeout) { // Set initial timeout function. myModuleTimeout = setTimeout(myModuleDataRefresh, Drupal.settings.mymodule.timeout); } }
The timeout function set above looks something like this:
// Similar to the myModuleTimeout variable set above, this will be // used to determine if the interval has been set. It can also be used to // stop the interval with a call to clearInterval(). var myModuleInterval = 0; var myModuleDataRefresh = function () { // Get latest data. var callbackPath = Drupal.settings.mymodule.callback_url; // Make a call to our menu callback to refresh data, then change the interface to reflect new values. $.ajax({ url: callbackPath, type: 'GET', dataType: 'json', error: function() { // Do any necessary error handling. In this case, I simply // ignore errors and keep the current values in place. }, success: function(data) { if (data.SomePropertyY) { // Replace the inner HTML of an element with the updated value. $('#an-id-tag-defined-in-the-block-code' + Drupal.settings.mymodule.nid).html(data.SomePropertyY); } } }); if (!myModuleInterval) { // Set the interval on which to call this function again. myModuleInterval = setInterval(myModuleDataRefresh, Drupal.settings.mymodule.cache_lifetime); } }
Note that the above code uses setInterval() instead of setTimeout(). The former works in much the same way, but only needs to be set once, rather than at the end of every timeout period.
The last step (which was actually my first step) is to create the JSON callback function (after defining a path via hook_menu()).
/** * Page callback: Sample JSON callback. */ function mymodule_get_json($node) { // Determine last updated. $updated = mymodule_data_last_updated($node); // Cache lifetime, defaults to 30 seconds. $minimum_lifetime = variable_get('mymodule_minimum_data_lifetime', 30); if ($_SERVER['REQUEST_TIME'] - $updated > $minimum_lifetime) { // An update is needed. mymodule_refresh_data($node); } // This function returns a simple object that drupal_json converts to JSON. $data = mymodule_get_data($node); drupal_json($data); // Returning NULL here also works. Using exit makes the callback faster since no additional code is run. exit; }
Tagged as: Drupal, Drupal 6, javascript, jQuery
Notes
1) Passing a string to setTimeout is unnecessary. You can pass a function variable directly, or even declare the callback inline like you do elsewhere.
2) Rather than repeatedly call setTimeout, you can call setInterval once for a repeating timer.
3) Using Drupal.behaviors this way is incorrect. Behavior functions are to be called any time new content is added to the document, so every ajax load will spawn a new setTimeout, causing your poll function to multiply in frequency. In your case, you are not applying new behaviors to the newly loaded content (which is a bug in itself), so this problem will only happen with other ajax requests.
4) Using local variables without declaring them with 'var' means they actually go in the default scope (in your case, the global window object). Don't do it.
I would suggest you let someone more skilled in JS review your code before posting.
Cron?
Why not have cron retrieve the page(s) from the 3rd party server and stick the result in Drupal's cache via cache_set()?
function mymodule_cron() { // check if an hour (or 5 minutes or whatever) have passed ... // Refresh the data $data = mymodule_refresh_data( ...); cache_set('mymodule:' . $nid, $data); } function mymodule_get_json( ...) { return cache_get('mymodule:', $nid); }This way you don't need client side trickery, and everything happens in the background.
@visitor Thanks for the
@visitor
Thanks for the feedback. The repeated calls to Drupal.attachBehaviors was causing the timeout to get stacked up; I had overlooked that the way I wrote the function would behave in this manner. The global scope variables was a result of my quickly trying to convert more complex JS into the above example. The above now uses both setTimeout and setInterval. The reason I didn't just call setInterval initially is that I'm dealing with 2 different time periods—the first being the time between when the page loads and when I want to make the first call, the second being the interval on which to make repeated calls.
I've updated the above to reflect these changes.
@Khalid
The above code does use cron to keep things up-to-date. The reason for the client-side callbacks is that the information in question (solar panel monitoring in this case) changes much more frequently than it is feasible to run cron (especially across hundreds of projects, some that may not be viewed very often). Utilizing client-side callbacks allows for pages that are currently being viewed to be kept up-to-date, to the minute.
Visitor is correct with
Visitor is correct with regards to #3, but there's an easy workaround. Check out the comment starting with line 10 in misc/drupal.js.
Also, printing drupal_json()'s return value is useless, because it does its own printing when you pass it a variable and never returns anything. It's drupal_to_js() which returns a JSON-serialized object as a string. Also, exiting after calling it should be unnecessary 99% of the time.
http://api.drupal.org/api/function/drupal_to_js/6
http://api.drupal.org/api/function/drupal_json/6
Also, consider using camel case in variable names to be used in JavaScript: "cacheLifetime" instead of "cache_lifetime." And note the capitalization of "JavaScript."
help
Could you email me a working-example of html page with your code?
zhlu@umich.edu