Skip to main content

Request aggregation in JSF 2 Ajax

Posted by driscoll on October 19, 2009 at 1:59 PM PDT

I've had a few requests for request aggregation, ala RichFaces queues, in JSF 2. This was deliberately not included in JSF 2.0, but it will be considered for JSF 2.1. The reason why is simple - there was simply not very much time, once all the base Ajax work was completed, to add any additional features. However, adding this functionality yourself isn't actually very hard. Here's an example of how.

For those not familiar with the idea of request aggregation, the idea is a pretty simple one: in cases where the user may generate a large number of requests (for instance, with the keyup event), you're going to want to wait and see if you can bundle the requests together, so you don't spam the server with thousands of tiny little requests.

The example will have two parts: a JSF page, and some backing JavaScript. The end result will be a program which accepts input, and echos it out to another part of the page via an Ajax request to the server. There's also a status area to show a little of what's going on during the call.

Here's the form:

   1 <h:form id="form1" prependId="false">
   2     <h:outputScript name="jsf.js" library="javax.faces" target="head"/>
   3     <h:outputScript name="javascript/aggregate.js" target="head"/>
   4 
   5     Output: <h:outputText id="out1" value="#{echo.str}"/>
   6     <br/>
   7     Input: <h:inputText id="in2" value="#{echo.str}" autocomplete="off"
   8                         onkeyup="aggregate('out1 in1', this)"/>
   9     <br/>
  10     Status:
  11     <br/>
  12     <textarea id="status" rows="10" cols="50" readonly="readonly"/>
  13 </h:form>

And here's the backing JavaScript:

   1 var increment = 1000; 
   2 var token;
   3 function aggregate(target, element) {
   4     window.clearTimeout(token);
   5     addStatusMessage("cleared request, requeued");
   6     var send = function send() {
   7         jsf.ajax.request(element, null, {render: target});
   8     };
   9     token = window.setTimeout(send, increment);
  10 }
  11 
  12 function addStatusMessage(message) {
  13     var statusElement = document.getElementById("status");
  14     var status = statusElement.value;
  15     var now = new Date();
  16     var timestamp = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
  17     status = timestamp + ' ' + message + '\n' + status;
  18     statusElement.value = status;
  19 }
  20 
  21 jsf.ajax.addOnEvent(function(data) {
  22     if (data.status === "begin") {
  23         addStatusMessage("request sent");
  24     }
  25 });

This program is simple enough that it's function should be pretty self evident - but in case you've never worked with JavaScript timers, here's the control flow.

When you type a character into the input field, the aggregate function is called (JSF page line 8). If an outstanding request is already there, it will be canceled (JavaScript line 4), and a status message will be written (JavaScript lines 12-19) to a readonly textarea in the page (JSF page line 12). Then, a new request will be submitted, which will go off after 1 second (JavaScript line 9). If you type a new character before the second has elapsed, then the first request is canceled (JS line 4), before being submitted again with a new value (JS line 9). If a whole second goes by without a new keypress, then the request will finally be sent to the server, which will also trigger calling the event function (JS lines 21 - 24), which in turn writes out a status (JS lines 12-19), to the textarea (JSF line 12).

So, all told, the aggregation code was about 6 lines of JavaScript. And while that may be a trifle annoying, I can only assume that anyone writing a component like Autocomplete will include this into the component so you never need see it.

As always, feel free to ask questions on this post in the comments.

Related Topics >>

Comments

How to do this?

Hi John, It's off topic for this blog post, but I'm interested in the following compontent: On facebook you have those more links on the homepage that display older posts. How would one implement this in Mojarra? Maybe you can create an example for this.

Already tried in the past but the approach has some problems.

Yes, we agree that this partially solves server flood problem. And we even used similar approach in the past. But looking closer to RichFaces queue we could see some features missed with the code listed in your post:

If some controls can't cancel requests of each other - them should use different aggregate methods. Let imagine the input with validation ajax capabilities using onkeyup and some form button. After some keyups appears - setTimeout used for the last one, but the user already clicked the button in the same time and we need both request to be sent sequentially. In the case of using two or more aggregate functions nobody guaranties that the order of requests will be kept. RichFaces has requests similarity mechanism which group similar requests in queue and send combined requests with different similarity one by one sequentially.

RichFaces queue requests wait for the previous request to be finished. And JSF queue - not. So concurrency problems will be still there even using this approach.

Also, there is some problems with stale form data if using jsf.ajax.request() will appears. The same as described there: RonanKER Forum Post

and b.t.w. this code

6 var send = function send() {
7 jsf.ajax.request(element, event, {render: target});
8 };
9 token = window.setTimeout(send, increment);


will most likelly cause memory leaks in IE

So maybe the problem is partially solved. But complete mechanism still should be implemented in order to use safely at production environments.

A couple comments

Since this behavior will be implemented by the end user, it's expected that they'd know which fields they want to aggregate or serialize. The point of the example was to give a *simple* example of use, not a comprehensive solution.

But this simple code probably solves 80% of folk's needs, since the typical use for this would be keypress events.

One substantial correction: The JSF queue is serial, so that concurrency problem you mention isn't there - though as you point out, ordering may matter to you.

Stale data would typically refer to using data that's no longer in the form - which isn't a problem, since the Ajax request will collect data as it appears at the moment the request is submitted. I think you meant that in the case of multiple requests, there is no atomaticity - but that would rarely, if ever, be a problem. Caching the data would result in stale data, but solve the atomaticity problem.

As for the memory leak - IE 6 leaks memory, generally, if you use DOM0 events, so this code is the least of your problems running under IE6 - though I'm not seeing the cyclic reference in the four lines you mention. IE7 fixes this partially, of course, by making the memory get collected when you leave the page's scope, and IE8 fixes it altogether (AFAIK).

My two cents

> Stale data would typically refer to using data that's no longer in the form - which isn't a problem, since the Ajax request will collect data as it appears at the moment the request is submitted
> I think you meant that in the case of multiple requests, there is no atomaticity - but that would rarely, if ever, be a problem. Caching the data would result in stale data, but solve the atomaticity problem.

There's a case when Ajax request collects data not at the moment when the request is submitted. This happens when jsf.ajax.request() is triggered by timer, but previous request hasn't completed yet (e.g. because of not fast DB). This can cause problems described in forum post to which Ilya referred.

> As for the memory leak - IE 6 leaks memory, generally, if you use DOM0 events, so this code is the least of your problems running under IE6 - though I'm not seeing the cyclic reference in the four lines you mention.
> IE7 fixes this partially, of course, by making the memory get collected when you leave the page's scope, and IE8 fixes it altogether (AFAIK).

Cyclic reference here is: window -> send -> element -> parentNode -> ... -> document -> parentWindow -> window. IE7 is still leaking in some cases (actually much less than IE6); cannot say anything about IE8.

No, it doesn't leak

No, the code I posted above doesn't leak.

The IE6 memory leak doesn't work the way you think it works, apparently. I tested it with sIEve, to be sure. I did, however, find a leak in the JSF Queue impl, which is embarrassing, so it wasn't wasted effort. That fix will be in 2.0.2. And the cyclic nature of IE leaks doesn't work the way you described it - there are two memory spaces, not one.

There was an additional bug in the example I posted, when run under IE - the event object in IE in global, not specific, so you can't just pass it to the timer. But that's easily fixed by changing it to a null, since it wasn't really required. (I've changed the code above to reflect that.)

I don't think I'm using the phrase "stale data" in the same way that you are, I guess - I come from an IT DB background, so I'm using it the way that any DB guy would. The data is current as of the submission time (launching the ajax request) to the backing store - just as if the user had pressed a button or struck a key. The data is consistent on the page - it may not be updated to the backing store, but that's what the ajax request is for. Making the request reflect what the state was at the time of user request (as opposed to submission) would result in stale data.