Reverse Ajax Hints and Tips

ScriptSession lifecycle

ScriptSessions are created when /dwr/engine.js is included in a page. By default, the lifecycle is maintained by the org.directwebremoting.impl.DefaultScriptSessionManager.

If you call the following javascript function:

dwr.engine.setNotifyServerOnPageUnload(true);

A remote DWR call will be made to the ScriptSessionManager when the page is unloaded, informing it to invalidate the ScriptSession. By default DWR makes this call synchronously to give the call the greatest chance of success. The synchronous nature of this call may result in a delay when closing the browser. If this is undesired you may pass an optional second parameter into dwr.engine.setNotifyServerOnPageUnload:

dwr.engine.setNotifyServerOnPageUnload(true, true);

The optional second parameter tells DWR to call the unloader asynchronously. This may result in the unloader being called inconsistently due to the browser closing the connection while unloading.

Note: In order for this to work DWR relies on the browser's "unload" event being fired. Unfortunately, browsers handle this event inconsistently and there may be very little DWR can do. If you are interested in following this issue you can watch DWR-151.

If setNotifyServerOnPageUnload is not set to true or fails to notify the server due to browser limitations ScriptSessions will be invalidated using a timeout strategy on the server - ScriptSessions inactive for five minutes or more will be invalidated.

Passing request information to a non-dwr thread

Non-dwr threads have no reference to the dwr-thread that created them. Because of this, WebContextFactory().get().getScriptSession() will return null in non-dwr threads. You will therefore need to pass data from DWR threads to non-DWR threads.

Obtaining the ScriptSessionManager from a non-dwr thread

The ScriptSessionManager can be obtained using the following code:

Container container = ServerContextFactory.get().getContainer();
ScriptSessionManager manager = container.getBean(ScriptSessionManager.class);

One thread per application is scalable, one thread per request is not

Most reverse ajax implementations require a separate thread to push data to clients. Spawning a separate thread for each DWR request is not scalable. We recommend that you use a thread pool in combination with an application scoped DWR creator, please see the Clock example in the dwr.war for a sample implementation.

Browser API - How to target specific ScriptSessions

DWR's Browser API contains several useful methods for updating browsers. Several of the Browser methods take a ScriptSessionFilter which will allow you to target specific ScriptSessions based on their attributes (see Setting differentiating attribute(s) on a ScriptSession).

How to use the Browser API with a ScriptSessionFilter to differentiate users:

  1. Implement a ScriptSessionFilter:

    public class TestScriptSessionFilter implements ScriptSessionFilter
    {
        public TestScriptSessionFilter(String attributeName)
        {
            this.attributeName = attributeName;
        }
    
        /* (non-Javadoc)
         * @see org.directwebremoting.ScriptSessionFilter#match(org.directwebremoting.ScriptSession)
         */
        public boolean match(ScriptSession session)
        {
            Object check = session.getAttribute(attributeName);
            return (check != null && check.equals(Boolean.TRUE));
        }
    
        private String attributeName;
    }
    
  2. Set an attribute on the ScriptSession:

    // Add the attribute into the ScriptSession sometime before using the Filter.
    ScriptSession scriptSession = WebContextFactory.get().getScriptSession();
    String attributeName = "attr";
    scriptSession.setAttribute(attributeName, true);
    

    Note - this must be done from a DWR initiated thread (WebContextFactory requires it).

    See Setting differentiating attribute(s) on a ScriptSession for more information.

  3. User the ScriptSessionFilter in your reverse AJAX thread:

    ScriptSessionFilter filter = new TestScriptSessionFilter(attributeName);
    Browser.withPageFiltered(page, filter, new Runnable()
    {
        public void run()
        {
            // Call a method on DWR's Util class which sets the value on an element on your HTML page with a id of "divID".
            Util.setValue("divID", "value of div");
        }
    });
    
    Or call a named function:
    ScriptSessionFilter filter = new TestScriptSessionFilter(attributeName);
    Browser.withPageFiltered(page, filter, new Runnable()
    {
        public void run()
        {
            // Call a named function from your html page. Note - The ScriptsSessions.addFunctionCall will only
            // send the function call to ScriptSessions matching TestScriptSessionFilter.
            ScriptSessions.addFunctionCall("yourJavaScriptFunctionName", arg1, arg2, etc.);
        }
    });
    
    Or add some arbitrary script:
    ScriptSessionFilter filter = new TestScriptSessionFilter(attributeName);
    Browser.withPageFiltered(page, filter, new Runnable()
    {
        public void run()
        {
            // Add script which will modify the document.title. on your html page.
            ScriptSessions.addScript(("document.title = 'My new title, from DWR reverse AJAX!';"));
        }
    });       
    

It is important to note that a few Browser methods require a WebContext which require that the requests come from a DWR thread. Currently the methods that require this are - withCurrentPageFiltered, withCurrentPage, and getTargetSessions. All other methods can be called safely from non-DWR threads.

Setting differentiating attribute(s) on a ScriptSession

One of the most common ways to differentiate users on the same page is by setting attributes on the ScriptSession and getting them in a reverse ajax thread. Since the ScriptSession does not exist until engine.js is included this cannot be done in an MVC controller or a JSP.

There are currently two best practices for setting an attribute on the ScriptSession:

1. Call a remote DWR method:

/**
 * This method should be remoted via DWR and generally called before reverse ajax is initialized.  
 * You may choose to call this method when your page which uses reverse AJAX is initialized, then
 * in your callback function you may initialize reverse AJAX (dwr.engine.setActiveReverseAjax(true);)
 * and be certain that the 
 */
public void remoteMethod() {
   String value = "someValue"; // this may come from the HttpSession for example
   ScriptSession scriptSession = WebContextFactory.get().getScriptSession();
   scriptSession.setAttribute("key", value);
}

2. Use a ScriptSessionListener which will be notified when ScriptSessions are created and destroyed. Please see the ScriptSessionListener section for more details.

/**
 * When a ScriptSession is created set a "userId" attribute on the ScriptSession.
 * In this case userId is an attribute you have set on the HttpSession that uniquely
 * identifies the user.
 */
public void sessionCreated(ScriptSessionEvent ev) {
    HttpSession session = WebContextFactory.get().getSession();
    String userId = (String) session.getAttribute("userId");
    ev.getSession().setAttribute("userId", userId);
}

Once the ScriptSession has been populated with an attribute, a reverse ajax thread can use the Browser API (recommended) with a ScriptSessionFilter to target specific ScriptSessions or retrieve the attribute from the ScriptSession (scriptSession.getAttribute()) to differentiate the users.

Differentiating ScriptSessions by request parameters

Another way to differentiate users on the same page is by the request parameters. In order to use this functionality, you must set the normalizeIncludesQueryString init-param to true. Once this is enabled, the ScriptSessionManager will include request parameters in the page name.
scriptSessionManager.getScriptSessionsByPage("/page?foo=bar")

If you would like to plug-in your own algorithm to determine the page name, you will need to implement your own org.directwebremoting.extend.PageNormalizer and plug it in using DWR's dependency injection mechanism in web.xml.

<servlet>
  <display-name>DWR Servlet</display-name>
  <servlet-name>dwr-invoker</servlet-name>  
  <description>Direct Web Remoter Servlet</description>
  <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
  <init-param>
       <param-name>org.directwebremoting.extend.PageNormalizer</param-name>
       <param-value>foo.bar.MyPageNormalizer</param-value>
  </init-param>
</servlet>

ScriptSessionListeners

ScriptSessionListeners allow you to take action when a ScriptSession has been created or destroyed. To configure a ScriptSessionListener you will need to execute code similar to the following:

Container container = ServerContextFactory.get().getContainer();
ScriptSessionManager manager = container.getBean(ScriptSessionManager.class);
ScriptSessionListener listener = new ScriptSessionListener() {
    public void sessionCreated(ScriptSessionEvent ev) {
        HttpSession session = WebContextFactory.get().getSession();
        String userId = (String) session.getAttribute("userId");
        ev.getSession().setAttribute("userId", userId);
    }

    public void sessionDestroyed(ScriptSessionEvent ev) { }
};
manager.addScriptSessionListener(listener);

It is important to note that ScriptSessionListeners must be added after DWR has initialized. There are generally two ways to do this:

  1. Extend the DWR servlet and execute the above code after the DWR servlet initializes.
  2. Execute the above code in a servlet that has a
    <load-on-startup />
    value larger than the DWR servlet