1

I'm using Http4s to mount a websocket service that I can use to communicate between this backend service and a UI (piping status updates and completion % for a batch job).

I'm using the BlazeBuilder Websocket Example to set up the service.

The service works, but what I'm trying to do is emit socket messages from within a class instance. For example, I want to instantiate a worker, pass a reference for the socket connection, and be able to emit data into that connection. Unfortunately, I'm having a very hard time making this work! It's much simpler in Python and JS.

See the code below, which is mostly the example code I linked above. In the place I'm calling Stream.emit(...), how can I pass a reference to that "toClient" and still emit to it? If I pass the toClient instance into a class instance, it doesn't seem to work.

case GET -> Root / "ws" =>
      val toClient: Stream[F, WebSocketFrame] = Stream.emit(Text("How can I do this from a class instance?"))
      val fromClient: Sink[F, WebSocketFrame] = _.evalMap { (ws: WebSocketFrame) =>
        ws match {
          case Text(t, _) => F.delay(println(t))
          case f => F.delay(println(s"Unknown type: $f"))
        }
      }
      WebSocketBuilder[F].build(toClient, fromClient)
CubemonkeyNYC
  • 273
  • 3
  • 17
  • It would be better to describe the actual problem you're trying to solve rather than how you want to solve it because the approach you've described is not the right approach for http4s/fs2. Why do you want to emit messages from a class? What end are you trying to achieve? – TheInnerLight May 23 '18 at 14:03

1 Answers1

0

You could use a MVar for the thread safe communication with the websocket.

Here is a example using Cats IO Effect:

final class WebSocketServer(implicit timer: Timer[IO]) extends Http4sDsl[IO] {

  implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)

  def start: IO[ExitCode] = {
    BlazeServerBuilder[IO]
      .bindHttp(8080)
      .withWebSockets(true)
      .withHttpApp(routes.orNotFound)
      .resource
      .use(_ => IO.never)
      .as(ExitCode.Success)
  }

  private[this] val routes: HttpRoutes[IO] = HttpRoutes.of[IO] {
    case GET -> Root / "ws" => {
      for {
        channel <- cats.effect.concurrent.MVar[IO].empty[List[WebSocketFrame]]
        webSocket <- {
          WebSocketBuilder[IO].build(
            send = fs2.Stream
              .eval(channel.take)
              .flatMap(fs2.Stream.emits(_))
              .repeat,
            receive = stream => {
              stream.evalMap {
                case Text(data, _)   => channel.put(List(Text("pong")))
                case unknown         => IO(println(s"Unknown type: $unknown"))
              }
            }
          )
        }
      } yield webSocket
    }
  }
}

If you want to send a message back to the client you have to put it into the MVar.

channel.put(List(Text("pong")))

The interesting part is the repeating stream that is polling the MVar for new messages to send back to the client of the WebSocket.

fs2.Stream.eval(channel.take).flatMap(fs2.Stream.emits(_).repeat
Markus Hettich
  • 544
  • 6
  • 12