Wednesday, 1 July 2009

Ajax Push is Easy

« From Ajax Push to JSF 2.0: ICEfaces on GlassFish | Main | The New New New Economy »

Let's take a step back and look at how we write Ajax applications. What would the ideal language for Ajax look like? What if we also want the application to be able to update the page at any time via Ajax Push (aka comet)? Need that be any more difficult than requesting "update the page"?

It can be easy, we just need to use the right language to define our Ajax application.

Since we're still a few steps back, let's see if we can just observe what our Ajax language looks like. We know the output of our application is HTML, mostly static but with the interesting parts dynamically generated. So the input language looks like HTML, but some of the elements variable in some way. We learned long ago that mixing executable code with markup is hard to maintain (remember JSP scriptlets?), so our markup language should incorporate dynamic elements by referring to values held by objects. It looks like this:

OK, so the name "inputText" is not ideal, and the XML namespaces are tedious; the point is that we can work with this language from a design perspective.)

Where's the Ajax though? When the user interacts with the page, how do we incrementally update it from the server without a full page refresh? (Should something this low-level really be the job of the application developer?) We've fully specified how the output markup relates to the objects in the model. It's should be the responsibility of the framework processing the language to update the page based on user input and application logic. Here's how it works:

  1. When the page is first rendered (fetched by the browser via a GET request) getters are called on the objects for dynamic values and a full page of markup is returned.
  2. When the user enters data, the form is encoded and submitted to the server via an XMLHttpRequest POST.
  3. The user input is decoded and applied to the model via setter methods.
  4. The page is rendered, changes from the existing page are determined, and an edit list is returned in the HTTP response.
  5. The browser applies the edits to the page.

In other words there was no reason for the application developer to be aware of Ajax at all, and this is exactly the process that takes place when you use the combination of JavaServer Faces and ICEfaces. Also notice that the rendering process that generates the initial page and the rendering process that determines the incremental updates are the same, considerably simplifying the Ajax pipeline.

But now what about Ajax Push? The application needs to change some text on the page independently of any user event; do we now need to rethink the web in terms of arbitrary executable clients on a message bus? After all, we just have some web pages that we want to update under application control, do we need a completely different set of abstractions?

Ajax Push is a revolutionary change in terms of what we can do on the web, but fortunately it's a small change from the above process. The only difference is that the page rendering (step 4) can be initiated by the application rather than the user, and the browser is listening for updates to arrive at any time, not just in response to user events. Here's how it works:

  1. When the page is first rendered the browser requests any page updates to be delivered when they are available.
  2. When something interesting happens in the application, the model is updated on the server, and the application renders the page.
  3. The page is rendered, changes from the existing page are determined, and an edit list is pushed to the browser.
  4. The browser applies the edits to the page.
  5. The browser requests any further page updates and the process repeats.

In other words, we can re-use our existing Ajax pipeline, and the application developer only needs to worry about one thing: requesting that a page be rendered. As you can imagine, this is only a few lines of code. With ICEfaces it looks like this:

Not only does the application developer not have to worry about low-level protocol details or messaging, they don't even have to worry about what has changed on the page -- this is all taken care of by the declared binding between the page and the model. The above code actually does a bit more, it also keeps track of all pages in each user's session, and all sessions in the specified group. Working with groups is useful for multi-user applications: for instance, when the moderator advances a presentation slide, all the users watching that slideshow should have their pages updated as well.

To summarize: we have dynamic pages that can be rendered on the server at any time, and the updates are pushed to the browser where the changes are applied to the DOM. From the application developer's point of view, that's all there is to it.

Under the Hood

Now let's look under the hood, is that simple as well? It's actually somewhat tricky, but the reasons for the difficulties are simple at least: browsers enforce per-host TCP connection limits, but JavaScript sandboxing does not allow easy communication between windows. Push notification requires a TCP connection to be held open, but each browser tab cannot have its own connection, otherwise the limited connections would be quickly exhausted when multiple tabs or windows are opened by the user. Yet it is difficult to share a single connection between browser tabs because the JavaScript sandboxes cannot easily communicate.

It turns out that the windows can communicate by sharing a cookie (it's a property of the connection, which is globally shared), allowing them to exchange rudimentary messages. Here ICEfaces takes a simpler approach than cometd. Rather than pass complex messages over the push channel (and be expected to propagate the full messages between browser windows), only the notification that page updates are available is pushed. The specific window then fetches its updates in the usual manner.

There is an additional wrinkle, however. What if the user opens multiple browser tabs to distinct push applications on the same server? In this case a single push connection must be shared between all of the applications and all of the tabs, requiring a single point of contact at the server which all applications must relay their push notifications to. It looks like this:

Fortunately, all of this complexity can be hidden behind a few simple APIs, as described above. (ICEfaces supports a variety of application servers in addition to GlassFish; for illustration, though, we show the custom integration with Grizzly as a way to optimize the server-side handling of push connections.)

Building Ajax Into the Web

Today we implement Ajax in JavaScript, but it is natural to consider incremental page update as an inherent part of the web. Rather than returning a full page response to a form POST, the browser could directly accept an edit list for the current DOM and apply it automatically. If connection limits were regarded per-frame (with each its own process so that when the Flash plugin crashes it doesn't take down the whole browser ... but I digress) then Ajax Push would also be very simple to implement.

Of course, with ICEfaces, Ajax Push is easy for the application developer now, independently of any upgrades we might want to consider for the web overall.

Posted by ted.goddard at 6:24 PM in Entries by Ted Goddard

 

[Trackback URL for this entry]

« July »
SMTWTFS
   1234
567891011
12131415161718
19202122232425
262728293031