目次

Version 12, last updated by lkuczera at Nov 28 01:17 UTC

Comet describes a model where the client sends an asynchronous request to the server. The request is hanging till the server has something interesting to respond. As soon as the server responds another request is made. The idea is to give the impression that the server is notifying the client of changes on the server. For more information about comet read the Wikipedia article.

To give you an idea of how cool comet can be check out the foursquare front page.

In Lift you can implement Comet in your app quite easily by extending CometActor like so:

package code {
package comet {

import net.liftweb._
import http._
import SHtml._ 
import net.liftweb.common.{Box, Full}
import net.liftweb.util._
import net.liftweb.actor._
import net.liftweb.util.Helpers._
import net.liftweb.http.js.JsCmds.{SetHtml}
import net.liftweb.http.js.JE.Str

class CometMessage extends CometActor {
	
  override def defaultPrefix = Full("comet")
		
  def render = bind("message" -> <span id="message">Whatever you feel like returning</span>)
		
  ActorPing.schedule(this, Message, 10000L)
		
  override def lowPriority : PartialFunction[Any,Unit] = {
    case Message => {
      partialUpdate(SetHtml("message", Str("updated: " + timeNow.toString)))
      ActorPing.schedule(this, Message, 10000L)
    }
  }
}
case object Message

}
}

Now, let me walk you through the lines here.

override def defaultPrefix = Full("comet")

This simply states which prefix you want to use in your templates. In this case we want to use the comet prefix.

def render = bind("message" -> <span id="message">Whatever you feel like returning</span>)

The render method returns a NodeSeq and is used to build the first response to the client.

ActorPing.schedule(this, Message, 10000L)

This line will be invoked once CometMessage is instantiated and sends a Message message back to our actor after 10 seconds.

override def lowPriority : PartialFunction[Any,Unit] = {
  case Message => {
    partialUpdate(SetHtml("message", Str("updated: " + timeNow.toString)))
    ActorPing.schedule(this, Message, 10000L)
  }
}

lowPriority is one of three methods (lowPriority, mediumPriority, hightPriority) you can override to process messages in your CometActor. The three methods lets you prioritize the messages.
The partialUpdate functions allows you to update specific fragments on the client side, in this case we’re changing the text of the node with id message.

CometMessage could be used in a template like this

<lift:surround with="default" at="content">
  <h2>Your first CometActor!</h2>
  <lift:comet type="CometMessage" name="Other">
    Current Time: <comet:message>Missing Clock</comet:message>
  </lift:comet>
</lift:surround> 

This is rather simple example with interaction of only one actor and one client.
Lets see how lift works out on example chat app. First chat server for handling incomming messages:

object ChatServer extends LiftActor with ListenerManager {
  var messages: List[Message] = Nil
  def createUpdate = messages
  override def lowPriority = {
	case msg: Message => {
		messages ::= msg
		updateListeners()
	}
  }
}
Note that ChatServer is an object.
And the message itself:
case class Message(user: String, msg: String)
To create comet “server” you need to mix in LiftActor with ListenerManager traits. ListenerManager is resposible for updating clients provided with createUpdate implementation.
override def lowPriority = {
   case msg: Message => {
      messages ::= msg
      updateListeners()
   }
}

Here messages received from clients are put into the list and update message is sent to all listening actors.

Now lets see how the client code looks like:

class Chat extends CometActor with CometListener {
  private var messages: List[Message] = Nil
  def registerWith = ChatServer
  private def renderMessages = <div>{messages.take(10).reverse.map(m => <li>{m.msg}</li>)}</div>
  def render = 	bind("chat", "input" -> ajaxForm(SHtml.text("", sendMessage _)),
			"messages" -> renderMessages)
  
  private def sendMessage(msg: String) = ChatServer ! Message("default", msg)
  override def lowPriority = {
	case msg:List[Message] => {
		messages = msg
		reRender(false)
	}
  }
}
Lets walk through it.
First register within server to receive updates
def registerWith = ChatServer
Each client holds its own messages buffer:
private var messages: List[Message] = Nil
Render last 10 messages in reverse order.
private def renderMessages = <div>{messages.take(10).reverse.map(m => <li>{m.msg}</li>)}</div>
Comet actor default render function wich binds ajax form in template and on ajax submit invokes sendMessage function with string argument (“_” – argument placeholder) taken from user input. And bind messages xml generated with renderMessages function to “messages” in template (in two lines of code).
def render = bind("chat", "input" -> ajaxForm(SHtml.text("", sendMessage _)),
			"messages" -> renderMessages)
Receive new messages from server
override def lowPriority = {
   case msg:List[Message] => {
	messages = msg
	reRender(false)
   }
}

reRender(false) is slightly confusing. reRender(false) means render only part that changes. CometActor has function
def fixedRender: Box[NodeSeq] wich marks that “fixed” part. Boolean parameter denotes if fixedRender shall be rerendered as well. In this example fixedRender is not used at all thus false argument.

Last but not least the template:

<lift:comet type="Chat" name="none" >
   <div><ul>
      <chat:messages/>
   </ul></div>
   <chat:input />
</lift:comet>
What could be enhanced in this example ? Attentive reader will note that user is fixed here. In next example you will see how to grab some very simple page flow with Lifts Comet.

Another problem is that messages will grow until memory runs out.