flaw-websocket-server/Flaw/Network/WebSocket/Wai.hs

Summary

Maintainability
Test Coverage
{-|
Module: Flaw.Network.WebSocket.Wai
Description: WebSocket server integration with WAI.
License: MIT
-}

{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, RankNTypes #-}

module Flaw.Network.WebSocket.Wai
  ( WebSocket()
  , webSocketWaiHandler
  ) where

import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import qualified Data.ByteString as B
import qualified Network.HTTP.Types as HT
import qualified Network.Wai as W
import qualified Network.Wai.Handler.WebSockets as WS
import qualified Network.WebSockets as WS
import qualified Wai.Routes as W

import Flaw.Network
import Flaw.Network.BiuoSocket

newtype WebSocket = WebSocket BiuoSocket deriving Socket

webSocketWaiHandler :: (sub -> W.Request -> IO (Either W.Response (WebSocket -> IO ()))) -> W.Handler sub
webSocketWaiHandler f W.Env
  { W.envSub = sub
  } reqData sendResponse = do
  eitherResponse <- f sub request
  sendResponse $ case eitherResponse of
    Left response -> response
    Right processSocket -> case WS.websocketsApp WS.defaultConnectionOptions (serverApp processSocket) request of
      Just response -> response
      Nothing -> W.responseLBS HT.badRequest400 [] "wrong websocket"
  where
    request = W.waiReq reqData
    serverApp processSocket pendingConnection = do
      -- accept connection
      connection <- WS.acceptRequest pendingConnection
      -- create socket
      socket@BiuoSocket
        { biuoSocketAliveVar = aliveVar
        , biuoSocketInQueue = inQueue
        , biuoSocketOutQueue = outQueue
        } <- atomically $ newBiuoSocket 1

      -- reading loop
      void $ forkIO $ flip finally (atomically $ writeTVar aliveVar False) $
        forever $ atomically . writeTBQueue inQueue =<< WS.receiveData connection

      -- writing loop
      void $ forkIO $ forever $ do
        delayVar <- registerDelay 5000000
        -- step loops until timeout
        let step = join $ atomically $ do
          let readPing = do
            timedOut <- readTVar delayVar
            unless timedOut retry
            return $ WS.sendPing connection B.empty
          let getBytes = do
            bytes <- readTQueue outQueue
            return $ do
              WS.sendBinaryData connection bytes
              step
          readPing `orElse` getBytes
        step

      -- run a handler
      processSocket $ WebSocket socket