packages feed

elm-websocket-1.0: src/Elm/WebSocket/Server.hs

{-# LANGUAGE OverloadedStrings #-}

module Elm.WebSocket.Server
  ( withWebSocketBroadcaster
  , newBroadcaster
  , broadcast
  ) where

import Elm.WebSocket.Types

import Control.Concurrent
import Control.Concurrent.STM.TChan
       (newBroadcastTChanIO, readTChan, dupTChan, writeTChan)

import Control.Monad (forever)
import Control.Monad.STM (atomically)
import Data.Aeson (FromJSON, ToJSON, encode, decode)
import Data.Foldable (traverse_)
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.WebSockets as WS
import qualified Network.WebSockets as WS

webSocketApp :: FromJSON a => ToJSON b => Broadcaster -> WebSocketServer a b -> WS.ServerApp
webSocketApp incomingBroadcasts server pendingConnection = do
  connection <- WS.acceptRequest pendingConnection
  WS.forkPingThread connection 30
  forkBroadcastThread connection incomingBroadcasts
  let loop = do
        WS.Text message <- WS.receiveDataMessage connection
        traverse_ (handleRequest connection server) (decode message)
        loop
  loop


handleRequest :: FromJSON a => ToJSON b => WS.Connection -> WebSocketServer a b -> a -> IO ()
handleRequest connection server request = do
  response <- server request
  traverse_ (WS.sendTextData connection . encode) response
  return ()

forkBroadcastThread :: WS.Connection -> Broadcaster -> IO ()
forkBroadcastThread connection incomingBroadcasts = do
  _ <-
    forkIO $ do
      channel <- atomically $ dupTChan incomingBroadcasts
      forever $ do
        message <- atomically $ readTChan channel
        WS.sendTextData connection message
  return ()

{-|
  Return a Wai.Application, which hosts both a WebSocket application, defined by the WebSocketServer,
  along with a non-websocket Wai.Application.

  This function also requires a Broadcaster, which is used to broadcast WebSocket messages to connected clients.

  The WebSocket application uses JSON as a wire format, so must have a FromJSON instance defined for the Request type,
  and a ToJSON instance defined for the Response type.
-}
withWebSocketBroadcaster :: FromJSON a => ToJSON b => Broadcaster -> WebSocketServer a b -> Wai.Application -> Wai.Application
withWebSocketBroadcaster connectedClients server =
  WS.websocketsOr WS.defaultConnectionOptions $ webSocketApp connectedClients server

{-|
  Create a Broadcaster, used to send messages to all connected clients.
-}
newBroadcaster :: IO Broadcaster
newBroadcaster = newBroadcastTChanIO

{-|
  Broadcast a message to all connected clients.
-}
broadcast :: ToJSON a => Broadcaster -> a -> IO ()
broadcast broadcaster message = atomically $ writeTChan broadcaster $ encode message