Section 10.5. Distributed Events


10.5. Distributed Events

Events, Feed, Listener, Messaging, MVC, Notification, Observer, PublishSubscribe, RSS, Refresh, Semantic, Synchronise, Synchronize, Update, WebFeed

Figure 10-9. Distributed Events


10.5.1. Developer Story

Devi's producing a web app for auditors at a financial institution, aiming to highlight substantial transactions. Relevant transactions are already published on the enterprise messaging system, and Devi transforms it into an RSS feed on the web server. The browser script then checks the feed every few seconds, and updates the view whenever it detects a substantial transaction occurred.

10.5.2. Problem

How do you decouple code in a complex application?

10.5.3. Forces

  • Ajax Apps involve at least two tiers: a browser tier and a web server tier. In practice, the web server tier is often dependent on further tiers and external systems.

  • Each tier can be quite complicated, containing many stateful entities (objects, HTML controls, or regular variables).

  • The state of all these entities must often be synchronized, in order to keep users and external systems up-to-date. The synchronization needs to occur within a tier as well as across tiersfor example, an HTML table needs to change whenever browser-side user preferences change, but also whenever the server-side database changes.

  • Keeping all these objects synchronized can become complexthere are about n2 possible message paths from one object to another.

10.5.4. Solution

Keep objects synchronized with an event mechanism. This is a classic software pattern applied to Ajax, related to the Observer (Gamma et al., 1995) and "Publisher-Subscribe" (Buschmann et al., 1995) patterns, and also a key feature in the classic "Model-View-Controller" architecture. The DOM already provides an mechanism for low-level events, but the events discussed here are more semantic in nature; i.e., related to business and application concepts such as "account deleted." Note that the term "event" is used in a broad manner to mean any subscription-based approach that alleviates the need for direct calls from source to destination. Any publish-subscribe messaging mechanism falls under this definition.

Here's a scenario to motivate the concept of events. Say you have 10 objects with inter-dependent states. That is, when one object changes, any number of the other nine must change accordingly. The naive implementation would endow each object with an understanding of the other nine objects. Each time it's changed, an object would then tell each other object how to update. Each object now knows the other nine intimately. When one changes, the other nine must be updateda major blow to encapsulation.

As with many programming problems, you can create a better solution by adding another layer of indirectionin this case, an event mechanism. Let each object broadcast changes instead of directly telling others how to respond. The changes should generally occur in semantic termsrather than saying "someone's clicked me," an object should say "counting module has begin," or, "transaction has successfully completed." And let any object register to be notified whenever a message like this occurs. For larger systems, thinking in terms of events is easier as it breaks down the synchronization logic. You have one simple task to make objects broadcast events whenever they occur. And you have a separate task to decide how objects should actually respond to events, if they care about them at all.

On the Web, this pattern can be applied in different ways:


Server-to-browser

Page elements can be kept in sync with server objects.


Browser-to-browser

Page elements can be kept in sync with each other.


Browser-to-server

Server objects can be kept in sync with each other.


Server-to-server

Server objects can be kept in sync with each other.

Browser-to-server and server-to-server are both feasible, but beyond the scope of the Ajax Patterns because they are more about server-side architecture.

As we saw in Chapter 7, JavaScript offers some event-handling support, but there's no generic event API. To use distributed events then, you'll need to roll your own event system, which is easier than it sounds. Server-to-Browser propagation works something like this. There's a Server Event Manager, with two responsibilities. First, it runs a Periodic Refresh (see earlier in this chapter) or participates in HTTP Streaming (Chapter 6) to check if any server activity has occurred. Second, it offers an event propagation facility so that interested browser entities can register to discover any changes. When a server change occurs, the Server Event Manager constructs an event object based on the server's output and passes it any interested listener. To do this, the manager must retain a collection of listeners and the event types they are listening to. The minimal set of services would then be:

  • addListener (eventType, listener)

  • removeListener (eventType, listener)

These listeners can be callback functions, like the callback function used by XMLHttpRequest. Or, they can be objects that contain a function with a standard callback name for the event type being registered, such as onUpdate( ).

With Browser-to-Browser propagation, you can also have a Central Server Manager to accept events and notifications. Alternatively, each object can be responsible for creating and propagating events specific to itself. That is, each object capable of generating events needs to allow other objects to register themselves for the events.

Observer is a special case of this pattern that arises frequently. The events are not user actions but change notifications. Event listeners are observing an object and responding to its state. Often, it's used to keep state in sync. An HTML table, for example, can render the latest state of a timetable object on the server side.

Finally, note that this pattern is somewhat speculative and open-ended, but the main purpose should be clear: to add a layer of intermediation so that objects can encapsulate their own responses to system activity, rather than being told what to do by other objects.

10.5.5. Decisions about Distributed Events

10.5.5.1. Will you publish a history or just the current state?

Given the nature of HTTP, server data must be retrieved periodicallythe server can't directly call the browser when something happens. If the server exposes only the current state, there are two problems:


Change detection

The browser can only infer a change has occurred by comparing the previous value, or perhaps by using a version ID or timestamp.


Intermediate states

The browser will miss values that occurred between updates.

Neither of these are showstoppers, so exposing only the current state may well be feasible. The alternative is to publish a history of states along with timestamps. That's more work to output and parse, and it also requires some form of storage on the server. If you are following that option, it's worth formatting the changes using a standard feed-based approach such as RSS or Atom. You'll benefit from the abundance of libraries for both browser and server. As a bonus, the service will be generic enough to be used by external clients, if that's a requirement.

10.5.5.2. For observer-style events, will you propagate the details of what's changed, or just point to the object that's changed?

Often, events concern an object that's changed state. Sometimes, you only need to propagate a pointer to the object; the recipient will then interrogate the object as required. Other times, you need to send the change itself. The latter approach is useful for functionality that requires not just the object's current state, but the nature of the change. For example, imagine you have an auditing function that logs whenever a budget balance has been increased. It will be much easier if the change event indicates that an increase has occurred. Otherwise, it will have to manually compare the budget against its previous state. For server-to-browser events, indicating the change may be better for performance since it may alleviate a follow-up call to the server.

10.5.5.3. What information will accompany the event notification?

When an event occurs, there's usually several pieces of information to pass to listenersfor example, a source object (an object that's just changed), the nature of the change or action that's occurred, and meta-information such as event time and unique ID. You can pass this information in different ways:


String message

Pass the information as a single string.


Single event object

Pass in a single event object containing attributes for each piece of information.


Parameter list

Pass the information as separate parameters.

Each style has its strengths. A string message is the most flexible approach and has the benefit of being a portable format that will work on the server as well. Unfortunately, a string message must often be parsed and formatted to convert to and from useful JavaScript values. A single event object is easier to manipulate and, like a string message, can usually be extended without breaking existing codethe callback function still takes a single value. You can create a factory function to create the event and expose properties, so its interface can be made explicit if you so desire. Finally, a parameter list makes for a cleaner callback function implementation, since you don't have to extract variables from a wrapper object. However, a long list of parameters is cumbersome and difficult to maintain.

10.5.5.4. Will events be processed synchronously or asynchronously?

The simplest way to handle events is synchronously. As soon as something happens, the event manager immediately notifies all interested parties. All this happens in the same thread of execution, so each event handler becomes a bottleneckthe main program flow that triggered the event won't be able to proceed until each event handler has executed.

If some event handlers are slow, you can get more stable performance by handling events asynchronously. Here, the manager maintains a collection of pending events. Each time a new event arises, it simply adds it to the collection and returnsa very quick operation. Using a repeating timer (see Scheduling [Chapter 7]), the manager periodically pulls off pending events and notifies listeners.

There are various decisions involved in asynchronous event handling. First, what sort of collection do you use? A queue is most common, to ensure that events are handled in the order they arise. But sometimes a stack is more appropriate, so that if the manager falls behind, at least the most recent events will have been handled. Another decision is scheduling of the event handler, the object that picks events off the collection. The simplest style is a pure repeating timer, but if the handling takes too long (longer than the timer interval), you'll end up with multiple processes picking off events. One way to prevent this situation is to have the event handler monitor its own progress and cease activity after a certain time has elapsed.

10.5.6. Real-World Examples

10.5.6.1. ActiveMQ Servlet Adaptor

ActiveMQ (http://activemq.codehaus.org/) is an open source implementation of Java Messaging Service (JMS) (http://java.sun.com/products/jms/), an official Sun standard for enterprise messaging. As such, it provides a way to pass Java objects and strings between different processes, and includes a publish-subscribe mechanism allowing a process to subscribe for all messages in a particular "topic."

Normally, the processes run server side, but using a servlet adaptor, ActiveMQ effectively gives the web app, through JavaScript, full ability to send and receive messages.

10.5.6.2. MapBuilder

MapBuilder (http://mapbuilder.sourceforge.net) is a framework for mapping web sites, heavily influenced by MVC. The model holds application data such as current maps, positions, and dimensions. The configuration process wires each model to a number of interested widgets, all of which receive notifications when the model has changed.

10.5.6.3. Dojo Events Library

Dojo (http://dojotoolkit.org/download) is a comprehensive framework aiming to simplify JavaScript development. One thing it does is enhance JavaScript's standard event management. This includes publish-subscribe functionality. You register one or more functions as publishers and one or more functions as subscribers. When a publisher function is called, all of the subscriber functions will also be called.

10.5.6.4. LivePage Library

LivePage (http://twisted.sourceforge.net/TwistedDocs-1.2.0/howto/livepage.html), mentioned in HTTP Streaming (Chapter 6) examples, is a framework based around Distributed Events.

10.5.7. Code Refactoring: AjaxPatterns Distributed Events Wiki Demo

The Basic Wiki Demo (http://ajaxify.com/run/wiki) has a single callback function that serves two purposes: to parse the incoming message and to display it to the user. That's okay for a simple application, but what if we want to scale up the display operation by displaying different messages in different ways or performing some action when a single message has changed? It won't be a great surprise that Distributed Events are one way to make the browser script more modular, and this refactoring shows how.

10.5.7.1. Refactoring to an event mechanism

The first refactoring lays the groundwork for a richer message handling by introducing an event mechanism. There are some minor user-interface differences, for coding convenience. For example, instead of a single "synchronize" point, downloading and uploading are split into independent timing mechanisms; there's no more background color change while waiting for a message to upload; and a One-Second Spotlight (Chapter 16) effect now occurs when a message has updated, to compensate for the loss of color change. Also, note that a different version of ajaxCaller is used, which allows a callback object to be specified in addition to a callback function.

A model object has been introduced to track the state of each message and to play the role of an event manager, notifying listeners of changes. One type of listener receives notification of any new messages. The other type receives notification of updates to a specific message. New message listeners are held in a single array. Update listeners are held in an array of arrays, with all subscribers to a particular message held in an array that's keyed on the message ID:

   newMessageListeners: new Array( ),   messageUpdateListenersById: new Array( ),   addNewMessageListener: function(listener) {     this.newMessageListeners.push(listener);   },   addMessageUpdateListener: function(messageId, listener) {     var listeners = this.messageUpdateListenersById[messageId];     listeners.push(listener);   }, 

Notification then works by iterating through the collection of relevant listeners:

   notifyNewMessageListeners: function(newMessage) {       for (i=0; i<this.newMessageListeners.length; i++) {         this.newMessageListeners[i](newMessage);       }   },   notifyMessageUpdateListeners: function(updatedMessage) {     var listenersToThisMessage =       this.messageUpdateListenersById[updatedMessage.id];     for (i=0; i<listenersToThisMessage.length; i++) {       listenersToThisMessage[i](updatedMessage);     }   } 

How do these events arise? The model object must be started manually and will then periodically poll the server:

   start: function( ) {     this.requestMessages( );     setInterval(this.requestMessages, DOWNLOAD_INTERVAL);   }   ...   requestMessages: function( ) {     ajaxCaller.getXML("content.php?messages", messageModel.onMessagesLoaded);   }, 

As before, the server provides an XML Message (Chapter 9) describing all messages. The model steps through each message in the XML, constructing an equivalent JavaScript object. If the message ID is unknown, the new message listeners are notified, and an array of update listeners is also created for this new message. If the message differs from the current message with the same ID, it's changed, so all the update listeners are notified. Recall that the message update notification is fine-grained: only listeners to a particular message ID are notified; hence the extraction of a message-specific list of listeners.

   onMessagesLoaded: function(xml, callingContext) {     var incomingMessages = xml.getElementsByTagName("message");     for (var messageCount=0; messageCount<incomingMessages.length;           messageCount++) {       var messageNode = incomingMessages[messageCount];       var content = this.getChildValue(messageNode, "content");       content = (content==null ? "" : unescape(content));       var incomingMessage = {         id: this.getChildValue(messageNode, "id"),         lastAuthor: this.getChildValue(messageNode, "lastAuthor"),         ranking: this.getChildValue(messageNode, "ranking"),         content: content       };       var currentMessage = this.messagesById[incomingMessage.id];       if (!currentMessage) {         this.messageUpdateListenersById[incomingMessage.id]=new Array( );         this.notifyNewMessageListeners(incomingMessage);       } else if (!this.messagesEqual(incomingMessage, currentMessage)) {         this.notifyMessageUpdateListeners(incomingMessage);       }       this.messagesById[incomingMessage.id] = incomingMessage;     }   },   getChildValue: function(parentNode, childName) {     var childNode = parentNode.getElementsByTagName(childName)[0];     return childNode.firstChild == null ? null : childNode.firstChild.nodeValue;   },   messagesEqual: function(message1, message2) {     return    message1.lastAuthor == message2.lastAuthor             && message1.ranking == message2.ranking             && message1.content == message2.content;   } 

A new messagesDiv object has also been created to encapsulate the message-handling logic. On startup, it subscribes for notification of new messages. For each message, it performs a similar function to what was previously done on each update: it creates all the message information and appends to the page, along with a newly introduced visual effect (courtesy of Scriptaculous; see http://script.aculo.us).

   start: function( ) {     messageModel.addNewMessageListener(this.onNewMessage);   },   ...   onNewMessage: function(message) {     var messageArea = document.createElement("textarea");     messageArea.className = "messageArea";     messageArea.id = message.id;     messageArea.serverMessage = message;     ...     messageDiv.appendChild(lastAuthor);     messageDiv.appendChild(messageArea);     ...     $("messages").appendChild(messageDiv);     Effect.Appear(messageDiv);     ...   } 

The messageDiv has another responsibility: it must update the display when a message has updated. Thus, it registers itself as a listener on each message. The easiest way to do this is upon adding each new message:

   onNewMessage: function(message) {     ...     messageModel.addMessageUpdateListener(message.id, function(message) {       var messageDiv = $("messageDiv" + message.id);       var lastAuthor = messageDiv.childNodes[0];       var messageArea = messageDiv.childNodes[1];       if (messageArea.hasFocus) {         return;       }       lastAuthor.innerHTML = message.id + "."         + "<em>" + message.lastAuthor + "</em>"+"."         + message.ranking;       messageArea.value = message.content;       Effect.Appear(messageDiv);     });   }, 

Compared to the previous version, we're now only redrawing a message when it's actually changed. Using an event mechanism has helped to separate the logic out. Now, it's the messageDiv itself that decides how it will look after a message comes in, which is much more sane than the message-receiving callback making that decision.

10.5.7.2. Introducing a watchlist

The refactoring above wouldn't be very useful if we stopped with an event mechanism. Good for your work experience perhaps, but we haven't yet added any functionality to justify the effort; it's basically the same application as before. Not to worry Watchlist Wiki Demo (http://ajaxify.com/run/wiki/events/watchlist) to the rescue! A new watchlist monitors interesting messages, so that when a message you're watching is updated (by you or someone else), the watchlist will add a summary line.

To start with, the HTML now includes a watchlist table:

   <div >     <table >       <tr>         <th>Author</th>         <th>Message</th>       </tr>       <tbody ></tbody>     </table>   </div> 

Which messages are in your watchlist? That's determined by a new checkbox control, one per message:

 onNewMessage: function(message) {   ...   var watching = document.createElement("input");   watching.type = "checkbox";   watching.messageId = message.id;   watching.onclick = onWatchingToggled;   ... } 

When the user wants to watch a message, she selects its checkbox. A single function updates the watchlist for all chosen messages. Remember that message update events are fine-grained, so we need to ensure this callback is registered to receive notifications for all the chosen messages and nothing else. So when a user deselects a message, we'll unregister the function as a listener on that message. Note that this functionality necessitated the creation of an function to unregister listeners, which was never required in the previous version.

 function onWatchingToggled(event) {   event = event || window.event;   var checkbox = event.target || event.srcElement;   if (checkbox.checked) {     messageModel.addMessageUpdateListener(checkbox.messageId, onWatchedMessageUpdate);   } else {     messageModel.removeMessageUpdateListener(checkbox.messageId,          onWatchedMessageUpdate);   } } 

onWatchedMessageUpdate will now receive notification of any new messages that are being watched. It simply adds a summary row to the table and runs a visual effect:

 function onWatchedMessageUpdate(message) {   var summary = message.content;   if (summary.length > 35) {     summary =   summary.substring(0, 15) + "..."               + summary.substring(summary.length - 15);   }   var row = document.createElement("tr");   var authorCol = document.createElement("td");   authorCol.className = "authorSummary";   authorCol.innerHTML = message.author;   row.appendChild(authorCol);   var contentCol = document.createElement("td");   contentCol.className = "contentSummary";   contentCol.innerHTML = summary;   row.appendChild(contentCol);   if ($("watchlistBody").childNodes.length > 0) {     $("watchlistBody").insertBefore(row, $("watchlistBody").childNodes[0]);   } else {     $("watchlistBody").appendChild(row);   }   Effect.Appear(row); } 

We now have two independent functions that receive notifications of new messages arriving from the server. Each can use the information however it pleases. This is a much more scalable approach than having the server message recipient dictate how the browser should respond.

10.5.8. Related Patterns

10.5.8.1. Periodic Refresh, HTTP Streaming

For server-to-browser propagation, Periodic Refresh (see earlier) or HTTP Streaming (Chapter 6) is required.

10.5.8.2. RESTful Service

Distributed Events usually involve a browser element observing a server-side entity. REST is ideal for this purpose as it provides a simple, standard way to exposes server state.

10.5.8.3. XML Data Island

If the server responds with XML and you need to retain state locallye.g., to track differencesan XML Data Island (Chapter 11) would be useful. Under some technologies illustrated in that pattern, XML Data Islands allow for automated updateswhen the data island changes, then a control is updated, and vice versa.

10.5.9. Metaphor

The old newspaper analogy still works. People can subscribe to any number of newspapers, and each newspaper can have any number of subscribers. The algorithm does not explicitly mention any particular subscriber; rather, when a newspaper comes out, it simply loops through each subscriber and sends a copy to each of them.




Ajax Design Patterns
Ajax Design Patterns
ISBN: 0596101805
EAN: 2147483647
Year: 2007
Pages: 169

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net