Async response

List of normal responding methods:

  • respondView: responds view template file, with or without layout

  • respondInlineView: responds embedded template (not separate template file), with or without layout

  • respondText("hello"): responds a string without layout

  • respondHtml("<html>...</html>"): same as above, with content type set to “text/html”

  • respondJson(List(1, 2, 3)): converts Scala object to JSON object then responds

  • respondJs("myFunction([1, 2, 3])")

  • respondJsonP(List(1, 2, 3), "myFunction"): combination of the above two

  • respondJsonText("[1, 2, 3]")

  • respondJsonPText("[1, 2, 3]", "myFunction")

  • respondBinary: responds an array of bytes

  • respondFile: sends a file directly from disk, very fast because zero-copy (aka send-file) is used

  • respondEventSource("data", "event")

Xitrum does not automatically send any default response. You must explicitly call respondXXX methods above to send response. If you don’t call respondXXX, Xitrum will keep the HTTP connection for you, and you can call respondXXX later.

To check if the connection is still open, call channel.isOpen. You can also use addConnectionClosedListener:

addConnectionClosedListener {
  // The connection has been closed
  // Unsubscribe from events, release resources etc.
}

Because of the async nature, the response is not sent right away. respondXXX returns ChannelFuture. You can use it to perform actions when the response has actually been sent.

For example, if you want to close the connection after the response has been sent:

import io.netty.channel.{ChannelFuture, ChannelFutureListener}

val future = respondText("Hello")
future.addListener(new ChannelFutureListener {
  def operationComplete(future: ChannelFuture) {
    future.getChannel.close()
  }
})

Or shorter:

respondText("Hello").addListener(ChannelFutureListener.CLOSE)

WebSocket

import scala.runtime.ScalaRunTime
import xitrum.annotation.WEBSOCKET
import xitrum.{WebSocketAction, WebSocketBinary, WebSocketText, WebSocketPing, WebSocketPong}

@WEBSOCKET("echo")
class EchoWebSocketActor extends WebSocketAction {
  def execute() {
    // Here you can extract session data, request headers etc.
    // but do not use respondText, respondView etc.
    // To respond, use respondWebSocketXXX like below.

    log.debug("onOpen")

    context.become {
      case WebSocketText(text) =>
        log.info("onTextMessage: " + text)
        respondWebSocketText(text.toUpperCase)

      case WebSocketBinary(bytes) =>
        log.info("onBinaryMessage: " + ScalaRunTime.stringOf(bytes))
        respondWebSocketBinary(bytes)

      case WebSocketPing =>
        log.debug("onPing")

      case WebSocketPong =>
        log.debug("onPong")
    }
  }

  override def postStop() {
    log.debug("onClose")
    super.postStop()
  }
}

An actor will be created when there’s request. It will be stopped when:

  • The connection is closed

  • WebSocket close frame is received or sent

Use these to send WebSocket frames:

  • respondWebSocketText

  • respondWebSocketBinary

  • respondWebSocketPing

  • respondWebSocketClose

There’s no respondWebSocketPong, because Xitrum will automatically send pong frame for you when it receives ping frame.

To get URL to the above WebSocket action:

// Probably you want to use this in Scalate view etc.
val url = absWebSocketUrl[EchoWebSocketActor]

SockJS

SockJS is a browser JavaScript library that provides a WebSocket-like object, for browsers that don’t support WebSocket. SockJS tries to use WebSocket first. If that fails it can use a variety of ways but still presents them through the WebSocket-like object.

If you want to work with WebSocket API on all kind of browsers, you should use SockJS and avoid using WebSocket directly.

<script>
  var sock = new SockJS('http://mydomain.com/path_prefix');
  sock.onopen = function() {
    console.log('open');
  };
  sock.onmessage = function(e) {
    console.log('message', e.data);
  };
  sock.onclose = function() {
    console.log('close');
  };
</script>

Xitrum includes the JavaScript file of SockJS. In your view template, just write like this:

...
html
  head
    != jsDefaults
...

SockJS does require a server counterpart. Xitrum automatically does it for you.

import xitrum.{Action, SockJsAction, SockJsText}
import xitrum.annotation.SOCKJS

@SOCKJS("echo")
class EchoSockJsActor extends SockJsAction {
  def execute() {
    // To respond, use respondSockJsXXX like below

    log.info("onOpen")

    context.become {
      case SockJsText(text) =>
        log.info("onMessage: " + text)
        respondSockJsText(text)
    }
  }

  override def postStop() {
    log.info("onClose")
    super.postStop()
  }
}

An actor will be created when there’s a new SockJS session. It will be stopped when the SockJS session is closed.

Use these to send SockJS frames:

  • respondSockJsText

  • respondSockJsClose

See Various issues and design considerations:

Basically cookies are not suited for SockJS model. If you want to authorize a
session, provide a unique token on a page, send it as a first thing over SockJS
connection and validate it on the server side. In essence, this is how cookies
work.

To config SockJS clustering, see Clustering with Akka.

Chunked response

To send chunked response:

  1. Call setChunked

  2. Call respondXXX as many times as you want

  3. Lastly, call respondLastChunk

Chunked response has many use cases. For example, when you need to generate a very large CSV file that does may not fit memory, you can generate chunk by chunk and send them while you generate:

// "Cache-Control" header will be automatically set to:
// "no-store, no-cache, must-revalidate, max-age=0"
//
// Note that "Pragma: no-cache" is linked to requests, not responses:
// http://palizine.plynt.com/issues/2008Jul/cache-control-attributes/
setChunked()

val generator = new MyCsvGenerator

generator.onFirstLine { line =>
  val future = respondText(header, "text/csv")
  future.addListener(new ChannelFutureListener {
    def operationComplete(future: ChannelFuture) {
      if (future.isSuccess) generator.next()
    }
  }
}

generator.onNextLine { line =>
  val future = respondText(line)
  future.addListener(new ChannelFutureListener {
    def operationComplete(future: ChannelFuture) {
      if (future.isSuccess) generator.next()
    }
  })
}

generator.onLastLine { line =>
  val future = respondText(line)
  future.addListener(new ChannelFutureListener {
    def operationComplete(future: ChannelFuture) {
      if (future.isSuccess) respondLastChunk()
    }
  })
}

generator.generate()

Notes:

  • Headers are sent on the first respondXXX call.

  • You can send optional trailing headers at respondLastChunk

  • Page and action cache cannot be used with chunked response.

Using chunked response together with ActorAction, you can easily implement Facebook BigPipe.

Forever iframe

Chunked response can be used for Comet.

The page that embeds the iframe:

...
<script>
  var functionForForeverIframeSnippetsToCall = function() {...}
</script>
...
<iframe width="1" height="1" src="path/to/forever/iframe"></iframe>
...

The action that responds <script> snippets forever:

// Prepare forever iframe

setChunked()

// Need something like "123" for Firefox to work
respondText("<html><body>123", "text/html")

// Most clients (even curl!) do not execute <script> snippets right away,
// we need to send about 2KB dummy data to bypass this problem
for (i <- 1 to 100) respondText("<script></script>\n")

Later, whenever you want to pass data to the browser, just send a snippet:

if (channel.isOpen)
  respondText("<script>parent.functionForForeverIframeSnippetsToCall()</script>\n")
else
  // The connection has been closed, unsubscribe from events etc.
  // You can also use ``addConnectionClosedListener``.

Event Source

See http://dev.w3.org/html5/eventsource/

Event Source response is a special kind of chunked response. Data must be UTF-8.

To respond event source, call respondEventSource as many time as you want.

respondEventSource("data1", "event1")  // Event name is "event1"
respondEventSource("data2")            // Event name is set to "message" by default