Search code examples
scalawebsocketplayframework-2.1

Can I use a single Play WebSocket for both broadcasting and private messages?


I need to send a private response to the client for which I'm currently handling a Play WebSocket message, while for other messages I need to broadcast responses to all clients.

In the Play chat example, incoming messages are immediately offloaded to a single Actor:

case Connected(enumerator) => 
  // Create an Iteratee to consume the feed, assuming all requests have
  // a JSON "text" node, and delegate all to the Akka Actor:
  val iteratee = Iteratee.foreach[JsValue] { event =>
    default ! Talk(username, (event \ "text").as[String])
  }.map { _ =>
    default ! Quit(username)
  }
  (iteratee,enumerator)

Above, once the connection is approved enumerator is passed back, which refers to the single chatEnumerator that was already created by that same Actor:

val (chatEnumerator, chatChannel) = Concurrent.broadcast[JsValue]

I assume this does not allow me to send a message to just a single client? Should I remove the broadcast functionality and create and keep track of enumerators for each client, and then iterate myself? Or can I somehow get a reference to a client-specific enumerator in the foreach?

(I understand that built-in flexibility depends on the implementation, but this use case seems quite common to me. Like when using WebSockets with Socket.IO, I can easily send messages to either all clients, all clients except the sender of the current request, or just a single client. That's what I'm trying to achieve in Play 2.1.x as well.)


Solution

  • One approach is to interleave two enumerators using Enumerator.interleave.

    Therefore, you can create two pairs of (Enumerator, Channel) using Concurrent.broadcast twice, one for broadcasting the other for private connection, and interleave it. (Or may just use Concurrent.unicast for private enumerator, but I could not figure out how to use it.)

    Here's some sample play code which works on play 2.3.0 .

    object Application extends Controller {
      val (publicOut,publicChannel) = Concurrent.broadcast[String]
      def chat = WebSocket.using[String]{ request =>
        val (privateOut,privateChannel) = Concurrent.broadcast[String]
        val in = Iteratee.foreach{
          msg:String => if(msg.startsWith("@")){
            publicChannel.push("Broadcasted: " + msg)
          }else{
            privateChannel.push("Private: " + msg)
          }
        }
        val out = Enumerator.interleave(publicOut,privateOut)
        (in, out)
      }
    }
    

    Sending message to specific client will be a little bit complicated code, but a concept would be same. Create an Actor which holds pair of (Enumerator, Channel) per websocket and send a message to the actor.