Reverse-Ajax (Comet)

In normal Ajax call, the browser will receives the outcome only after the server has finished with the entire requested action. Certain actions may take a long time to complete. In this example, an Ajax request to book a flight. The controller is performing some long-running tasks while writing to the SheepJax commands, but none of those commands will be received by the client until all the action completes. In the meantime, the browser will just sit idle for few minutes, until the the action completes and the whole commands will get executed at once. This results in a poor user experience.
To solve this, we can use SheepJax Comet facility to allow server-side actions to send SheepJax commands on the client-side immediately as they get called, without waiting for the entire Ajax action to completes. This method is also popularly known as Reverse-Ajax, which describe the capability for the web-server to initiate a command to the browser.
By default, the Comet transport used by SheepJax is through long-polling. In the near future, when ASP.NET 4.5 comes out, SheepJax will also support WebSockets when it detects an HTML5-compliant browser.

JavaScript

On the JavaScript side, there is no difference between a normal SheepJax call and a Comet SheepJax.
$.sheepJax({
    url: "@Url.Action("BookFlight")",
    data: $("#book_flight_form").serialize(),
    commands: {
        CheckingSeatsAvailability: function(){
            // show check-availability progress-bar
        },
        SeatOK(seatInfo){
            // tick off the seat, show details, update progress, etc
        },
        SeatNotAvailable(seatInfo){
            // cross the seat, show details, update progress, etc
        },
        PaymentCleared(transactionRef){
            // "Hello, you are 2500 bucks poorer"
        },
        Completed(iterneraryPdfUrl){
            // "Congratulation. Your itenerary pdf has been emailed, or click here"
        }
        Failed(reason){
            // BIG RED SIRENE goes off
        }
    }
});

MVC Controller (with a busy action)

The server-side code on the Controller for a comet SheepJax is almost exactly identical to that of a normal SheepJax call.
To make a SheepJax comet, you simply change this normal SheepJax call:
SheepJax.Dynamic(cmd=> { /* blah */ });
.. into the following comet API, simply by attaching it with a Comet() extension method (from SheepJax.Comet namespace):
SheepJax.Dynamic().Comet(cmd=> { /* blah */ });
By adding this Comet() method, your action will return immediately, and instruct the browser to establish a long-polling channel to the SheepJax http-handler.
So here's the full code of our example.
using SheepJax;
using SheepJax.Comet;
public class BookingController: Controller
{
   [HttpPost]
   public ActionResult BookFlight(BookingDetails booking)
   {
      return SheepJax.Dynamic().Comet(cmd=>
      {
         cmd.CheckingSeatsAvailability();
         Parallel.Foreach(booking.Seats, seat=>
         {
            if(_backend.ReserveSeat(seat)) // slow...
               cmd.SeatOK(seat);
            else
            {
               cmd.SeatNotAvailable(seat);
               allSeatsOk = false;
            }
         }
         if(allSeatsOk)
         {
            cmd.Failed("One or more seats are not available");
            return;
         }

         if(_paymentGateway.TakePayment(booking.Payment)) // slow...
         {
            cmd.PaymentCleared(transactionRef);  // slow...
            var url = cmd.GenerateItenerary(booking);
            cmd.Completed(url);
         }
         else
            cmd.Failed("Payment declined");
      });
   }
}
On the example above, cmd acts a remote proxy to the client-side commands. Whenever you invoke this proxy, instead of waiting for the completion of the action, SheepJax will immediately invoke the function on the client-side.

MVC Controller (with asynchronous task, .NET4.0)

SheepJax comet API uses Task Parallel Library. It means that you can leverage the full asynchronous capabilities of .NET4.0 to reduce thread footprint of your web-server. (It also means you can fully leverage async/await syntax in C#5.0, covered in the next section)
Using Task library, when our action is waiting on slow external resources, the thread will be released back to the pool so it can be used to serve other web-requests. Here's our example code, rewritten using .NET4.0 Task library.
using SheepJax;
using SheepJax.Comet;
public class BookingController: Controller
{
   [HttpPost]
   public ActionResult BookFlight(BookingDetails booking)
   {
      return SheepJax.Dynamic().Comet(cmd=>
      {
         cmd.CheckingSeatsAvailability();
         return Task.Factory.ContinueWhenAll(booking.Seats.Select(seat=>
            _backend.ReserveSeatAsync(seat)   // async & callback
               .ContinueWith(t=> {
                  if(t.IsCompleted && t.Result)
                     cmd.SeatOK(seat);
                  else
                  {
                     cmd.SeatNotAvailable(seat);
                     allSeatsOk = false;
                  }
               })
         ).ContinueWith(tasks=>
         {
            if(!allSeatsOk)
            {
               cmd.Failed("One or more seats are not available");
               return EmptyTask();
            }
            
            return _paymentGateway.TakePaymentAsync(booking.Payment) // async & callback
               .ContinueWith(t=> {
                  if(t.IsCompleted && t.Result)
                  {
                     cmd.PaymentCleared(transactionRef);
                     return cmd.GenerateIteneraryAsync(booking) // async & callback
                        .ContinueWith(t2=> cmd.Completed(t2.Url));
                  }
                  
                  cmd.Failed("Payment declined");
                  return EmptyTask().
               }).Unwrap();
         }).Unwrap();
      });
   }
}

MVC Controller (async/await, C# 5.0)

That was one ugly piece of code. TPL is mad! That's why we have C#5.
In C#5, we can use async delegate on SheepJax comet. The .NET4.0 code above can be rewritten in C#5 into the following.
using SheepJax;
using SheepJax.Comet;
public class BookingController: Controller
{
   [HttpPost]
   public ActionResult BookFlight(BookingDetails booking)
   {
      return SheepJax.Dynamic().Comet(async cmd=>  // async comet Task
      {
         cmd.CheckingSeatsAvailability();
         
         await TaskEx.WhenAll(booking.Seats.Select(async seat=>
         {
            if(await _backend.ReserveSeatAsync(seat)) // async/await
               cmd.SeatOK(seat);
            else
            {
               cmd.SeatNotAvailable(seat);
               allSeatsOk = false;
            }
         }));
         
         if(allSeatsOk)
         {
            cmd.Failed("One or more seats are not available");
            return;
         }
         
         if(await _paymentGateway.TakePaymentAsync(booking.Payment)) // async/await
         {
            cmd.PaymentCleared(transactionRef);
            var url = await cmd.GenerateIteneraryAsync(booking); // async/await
            cmd.Completed(url);
         }
         else
            cmd.Failed("Payment declined");
      });
   }
}

Last edited Sep 22, 2011 at 4:49 PM by hendryluk, version 19

Comments

No comments yet.