Pure WS monad

This commit is contained in:
qvalentin 2023-02-19 11:41:32 +01:00
parent 5d3dced6f7
commit 865b69e799
12 changed files with 346 additions and 221 deletions

View File

@ -1,52 +1,51 @@
module BroadcastUserData module BroadcastUserData
( broadcastUserData, ( broadcastUserData,
broadCastToClientsGeneric,
MonadBroadcast (..),
) )
where where
import ClassyPrelude import ClassyPrelude
import Data.Aeson (encode) import Data.Aeson (encode)
import Network.WebSockets qualified as WS import qualified Network.WebSockets as WS
import State.ConnectedClientsState (MonadConnectedClientsRead (getConnctedClients))
import State.RoomDataState (MonadRoomDataStateRead (getRoomDataState))
import Types.AppTypes (HasConnectedClientState (..)) import Types.AppTypes (HasConnectedClientState (..))
import Types.ConnectionState (Client (..), ConnectedClients) import Types.ConnectionState (Client (..), ConnectedClients)
import Types.RoomsState (HasRoomsState (..)) import Types.RoomsState (HasRoomsState (..))
import Types.UsersData (UsersData (..)) import Types.UsersData (UsersData (..))
class (Monad m, MonadConnectedClientsRead m) => MonadBroadcast m where
broadCastToClients :: Text -> m ()
broadcastUserData :: broadcastUserData ::
( MonadIO m, ( MonadRoomDataStateRead m,
HasConnectedClientState env, MonadBroadcast m
HasRoomsState env,
MonadReader env m
) => ) =>
m () m ()
broadcastUserData = do broadcastUserData = do
putStrLn "broadcasting"
userWithOutRoom <- getUsersWithoutRoom userWithOutRoom <- getUsersWithoutRoom
roomsData <- ask >>= readMVar . getRoomsState roomsData <- getRoomDataState
let usersData = UsersData {usersWithOutRoom = userWithOutRoom, roomsData = roomsData} let usersData = UsersData {usersWithOutRoom = userWithOutRoom, roomsData = roomsData}
broadCastToClients $ (decodeUtf8 . toStrict . encode) usersData broadCastToClients $ (decodeUtf8 . toStrict . encode) usersData
getUsersWithoutRoom :: getUsersWithoutRoom ::
( MonadIO m, ( MonadConnectedClientsRead m
HasConnectedClientState env,
MonadReader env m
) => ) =>
m [Text] m [Text]
getUsersWithoutRoom = do getUsersWithoutRoom = map name . filter (not . joinedRoom) <$> getConnctedClients
state <- ask >>= readMVar . getConnectedClientState
return $ map name $ filter (not . joinedRoom) state broadCastToClientsGeneric ::
( MonadIO m,
MonadConnectedClientsRead m
) =>
Text ->
m ()
broadCastToClientsGeneric message = do
state <- getConnctedClients
liftIO $ broadcast message state
broadcast :: Text -> ConnectedClients -> IO () broadcast :: Text -> ConnectedClients -> IO ()
broadcast message clients = do broadcast message clients = do
putStrLn message putStrLn message
forM_ clients $ \client -> WS.sendTextData (conn client) message forM_ clients $ \client -> WS.sendTextData (conn client) message
broadCastToClients ::
( MonadIO m,
HasConnectedClientState env,
MonadReader env m
) =>
Text ->
m ()
broadCastToClients message = do
state <- getConnectedClientState <$> ask
liftIO $ withMVar state $ \currenState -> broadcast message currenState

View File

@ -10,7 +10,7 @@ import Types.AppTypes
import Types.ConnectionState (initConnectionsState) import Types.ConnectionState (initConnectionsState)
import Types.RoomsState (initRoomsState) import Types.RoomsState (initRoomsState)
import WebServer (runWebServer) import WebServer (runWebServer)
import WebSocket (runWebSocketServer) import WebSocket.Server (runWebSocketServer)
runBothServers :: IO () runBothServers :: IO ()
runBothServers = do runBothServers = do

View File

@ -5,7 +5,7 @@ module RoomDataHandler
) )
where where
import BroadcastUserData (broadcastUserData) import BroadcastUserData (MonadBroadcast, broadcastUserData)
import ClassyPrelude import ClassyPrelude
import Control.Monad.Except (MonadError, throwError) import Control.Monad.Except (MonadError, throwError)
import Data.Aeson (eitherDecodeStrict) import Data.Aeson (eitherDecodeStrict)
@ -16,27 +16,36 @@ import Network.Wai
consumeRequestBodyStrict, consumeRequestBodyStrict,
responseLBS, responseLBS,
) )
import State.ConnectedClientsState (MonadConnectedClientsRead)
import State.RoomDataState
( MonadRoomDataStateModify (setRoomDataState),
MonadRoomDataStateRead,
)
import Types.AppTypes (HasConnectedClientState) import Types.AppTypes (HasConnectedClientState)
import Types.RoomsState import Types.RoomsState
( HasRoomsState, ( HasRoomsState,
roomStateDiffers, roomStateDiffers,
updateRoomState, updateRoomState,
) )
import Types.WebEnv (HasWebEnv (getRequest), getRespond) import Types.WebEnv
( HasWebEnv (getRequest),
getRespond,
)
roomDataHandler :: roomDataHandler ::
( MonadIO m, ( MonadIO m,
HasWebEnv env, HasWebEnv env,
HasConnectedClientState env,
MonadReader env m, MonadReader env m,
MonadError ResponseReceived m, MonadError ResponseReceived m,
HasRoomsState env MonadRoomDataStateRead m,
MonadRoomDataStateModify m,
MonadBroadcast m
) => ) =>
m ResponseReceived m ResponseReceived
roomDataHandler = do roomDataHandler = do
newRoomData <- parseBodyOrBadRequest newRoomData <- parseBodyOrBadRequest
whenM (roomStateDiffers newRoomData) $ do whenM (roomStateDiffers newRoomData) $ do
updateRoomState newRoomData setRoomDataState newRoomData
broadcastUserData broadcastUserData
success success

View File

@ -82,7 +82,7 @@ class Monad m => MonadConnectedClientsRead m where
getConnctedClientsGeneric :: getConnctedClientsGeneric ::
( HasConnectedClientState env, ( HasConnectedClientState env,
MonadReader env m, MonadReader env m,
MonadUnliftIO m MonadIO m
) => ) =>
m ConnectedClients m ConnectedClients
getConnctedClientsGeneric = do getConnctedClientsGeneric = do

View File

@ -0,0 +1,14 @@
module State.RoomDataState
( MonadRoomDataStateRead (..),
MonadRoomDataStateModify (..),
)
where
import ClassyPrelude
import Types.RoomData
class Monad m => MonadRoomDataStateModify m where
setRoomDataState :: RoomsData -> m ()
class Monad m => MonadRoomDataStateRead m where
getRoomDataState :: m RoomsData

View File

@ -4,10 +4,12 @@ module Types.RoomsState
HasRoomsState (..), HasRoomsState (..),
roomStateDiffers, roomStateDiffers,
updateRoomState, updateRoomState,
getRoomState,
) )
where where
import ClassyPrelude import ClassyPrelude
import State.RoomDataState (MonadRoomDataStateRead (getRoomDataState))
import Types.RoomData (RoomsData) import Types.RoomData (RoomsData)
type RoomsState = MVar RoomsData type RoomsState = MVar RoomsData
@ -30,16 +32,23 @@ updateRoomState newData = do
_ <- swapMVar state newData _ <- swapMVar state newData
return () return ()
roomStateDiffers :: getRoomState ::
( HasRoomsState env, ( HasRoomsState env,
MonadIO m, MonadIO m,
MonadReader env m MonadReader env m
) => ) =>
m RoomsData
getRoomState = do
state <- getRoomsState <$> ask
readMVar state
roomStateDiffers ::
( MonadRoomDataStateRead m
) =>
RoomsData -> RoomsData ->
m Bool m Bool
roomStateDiffers newData = do roomStateDiffers newData = do
currentData <- ask >>= readMVar . getRoomsState not . eqIgnoreOrdering newData <$> getRoomDataState
return $ not $ eqIgnoreOrdering newData currentData
eqIgnoreOrdering :: (Eq a) => [a] -> [a] -> Bool eqIgnoreOrdering :: (Eq a) => [a] -> [a] -> Bool
eqIgnoreOrdering a b = length a == length b && all (`elem` b) a eqIgnoreOrdering a b = length a == length b && all (`elem` b) a

View File

@ -1,22 +1,33 @@
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
module WebServer (runWebServer) where module WebServer (runWebServer) where
import BroadcastUserData
( MonadBroadcast (..),
broadCastToClientsGeneric,
)
import ClassyPrelude hiding (decodeUtf8) import ClassyPrelude hiding (decodeUtf8)
import Control.Monad.Except import Control.Monad.Except
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai import Network.Wai
import Network.Wai.Handler.Warp (run) import Network.Wai.Handler.Warp (run)
import RoomDataHandler (roomDataHandler) import RoomDataHandler (roomDataHandler)
import Types.AppTypes import State.ConnectedClientsState
( Env (..), ( MonadConnectedClientsRead (..),
HasConnectedClientState (..), getConnctedClientsGeneric,
) )
import State.RoomDataState
( MonadRoomDataStateModify (..),
MonadRoomDataStateRead (getRoomDataState),
)
import Types.AppTypes (Env (..))
import Types.RoomsState import Types.RoomsState
( HasRoomsState (getRoomsState), ( HasRoomsState (getRoomsState),
getRoomState,
roomStateDiffers, roomStateDiffers,
updateRoomState, updateRoomState,
) )
@ -43,6 +54,18 @@ newtype WebApp env a = WebApp {unWebApp :: env -> IO (Either ResponseReceived a)
) )
via ReaderT env (ExceptTApp ResponseReceived) via ReaderT env (ExceptTApp ResponseReceived)
instance MonadConnectedClientsRead (WebApp WebEnv) where
getConnctedClients = getConnctedClientsGeneric
instance MonadRoomDataStateModify (WebApp WebEnv) where
setRoomDataState = updateRoomState
instance MonadRoomDataStateRead (WebApp WebEnv) where
getRoomDataState = getRoomState
instance MonadBroadcast (WebApp WebEnv) where
broadCastToClients = broadCastToClientsGeneric
getRequestPath :: getRequestPath ::
( MonadIO m, ( MonadIO m,
HasWebEnv env, HasWebEnv env,
@ -56,10 +79,11 @@ getRequestPath = do
app :: app ::
( MonadIO m, ( MonadIO m,
HasWebEnv env, HasWebEnv env,
HasConnectedClientState env,
MonadReader env m, MonadReader env m,
HasRoomsState env, MonadError ResponseReceived m,
MonadError ResponseReceived m MonadRoomDataStateModify m,
MonadRoomDataStateRead m,
MonadBroadcast m
) => ) =>
m ResponseReceived m ResponseReceived
app = requestPathHandler app = requestPathHandler
@ -67,10 +91,11 @@ app = requestPathHandler
requestPathHandler :: requestPathHandler ::
( MonadIO m, ( MonadIO m,
HasWebEnv env, HasWebEnv env,
HasConnectedClientState env,
MonadReader env m, MonadReader env m,
HasRoomsState env, MonadError ResponseReceived m,
MonadError ResponseReceived m MonadRoomDataStateModify m,
MonadRoomDataStateRead m,
MonadBroadcast m
) => ) =>
m ResponseReceived m ResponseReceived
requestPathHandler = do requestPathHandler = do

View File

@ -3,178 +3,43 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
module WebSocket (runWebSocketServer) where module WebSocket (WSEnv (..), wsApp, WSApp (..)) where
import BroadcastUserData (broadcastUserData) import BroadcastUserData
( MonadBroadcast (..),
broadcastUserData,
)
import ClassyPrelude import ClassyPrelude
import Data.Aeson import Data.UUID.V4 ()
import Data.UUID (UUID) import State.ConnectedClientsState (MonadConnectedClientsModify (..))
import Data.UUID.V4 (nextRandom) import State.RoomDataState (MonadRoomDataStateRead (..))
import Network.WebSockets qualified as WS import Types.ConnectionState (Client (..))
import State.ConnectedClientsState
( MonadConnectedClientsModify (..),
MonadConnectedClientsRead (getConnctedClients),
addWSClientGeneric,
getConnctedClientsGeneric,
removeWSClientGeneric,
updateWSClientGeneric,
)
import Types.AppTypes
( Env (..),
HasConnectedClientState (getConnectedClientState),
)
import Types.ConnectionState
( Client (..),
ConnectedClients,
)
import Types.RoomsState (HasRoomsState (..))
import Types.WebSocketMessages.WebSocketMessages import Types.WebSocketMessages.WebSocketMessages
( SetClientInfo (displayName), ( SetClientInfo (displayName),
WebSocketMessage (..), WebSocketMessage (..),
) )
import WebSocket.MonadWebSocketSession
data WSEnv = WSEnv import WebSocket.WSReaderTApp
{ appEnv :: Env,
connection :: WS.Connection,
clientId :: UUID
}
instance HasConnectedClientState WSEnv where
getConnectedClientState = connectedClientsState . appEnv
instance HasRoomsState WSEnv where
getRoomsState = roomsState . appEnv
class HasWSEnv a where
getConn :: a -> WS.Connection
getClientId :: a -> UUID
instance HasWSEnv WSEnv where
getConn = connection
getClientId = clientId
joinRoom ::
( MonadReader env m,
MonadConnectedClientsModify m,
HasWSEnv env
) =>
m ()
joinRoom = do
clientId <- getClientId <$> ask
updateWSClient clientId (\c -> c {joinedRoom = True})
updateClientName ::
( HasWSEnv env,
MonadReader env m,
MonadConnectedClientsModify m
) =>
SetClientInfo ->
m ()
updateClientName clientInfo = do
clientId <- getClientId <$> ask
updateWSClient clientId (\c -> c {name = displayName clientInfo})
disconnectWsClient ::
( HasWSEnv env,
MonadConnectedClientsModify m,
MonadReader env m
) =>
m ()
disconnectWsClient = do
clientId <- getClientId <$> ask
removeWSClient clientId
runWebSocketServer ::
( MonadIO m,
MonadReader Env m
) =>
m ()
runWebSocketServer = do
putStrLn "Websocket up at 127.0.0.1:9160"
wsApp' <- runWSApp
liftIO $ WS.runServer "127.0.0.1" 9160 wsApp'
runWSApp ::
( MonadIO m,
MonadReader Env m
) =>
m WS.ServerApp
runWSApp = do
env <- ask
return
( \pending -> do
conn <- WS.acceptRequest pending
uuid <- nextRandom
let wsEnv = WSEnv {appEnv = env, connection = conn, clientId = uuid}
WS.withPingThread conn 30 (return ()) $ unWSApp wsApp wsEnv
)
newClient ::
( MonadIO m,
HasWSEnv env,
MonadReader env m
) =>
SetClientInfo ->
m Client
newClient clientInfo = do
env <- ask
return $ Client {uuid = getClientId env, name = displayName clientInfo, conn = getConn env, joinedRoom = False}
newtype WSApp env a = WSApp {unWSApp :: env -> IO a}
deriving
( Functor,
Applicative,
Monad,
MonadReader env,
MonadIO,
MonadUnliftIO
)
via ReaderT env IO
class Monad m => MonadWebSocket m where
getTypedWSMessage :: FromJSON a => m a
instance MonadWebSocket (WSApp WSEnv) where
getTypedWSMessage = do
msg <- getMessage
case eitherDecodeStrict $ encodeUtf8 msg of
Right a -> return a
Left err -> do
sendMessage $ "Bad message: " <> pack err
getTypedWSMessage
instance MonadConnectedClientsModify (WSApp WSEnv) where
addWSClient = addWSClientGeneric
updateWSClient = updateWSClientGeneric
removeWSClient = removeWSClientGeneric
instance MonadConnectedClientsRead (WSApp WSEnv) where
getConnctedClients = getConnctedClientsGeneric
wsApp :: wsApp ::
( HasWSEnv env, ( MonadWebSocketSession m,
HasConnectedClientState env, MonadWebSocketSessionInit m,
HasRoomsState env, MonadConnectedClientsModify m,
MonadReader env m, MonadRoomDataStateRead m,
MonadUnliftIO m, MonadBroadcast m
MonadWebSocket m,
MonadConnectedClientsModify m
) => ) =>
m () m ()
wsApp = do wsApp = do
msg <- getTypedWSMessage msg <- getTypedWSMessage
print msg
client <- newClient msg client <- newClient msg
addWSClient client addWSClient client
broadcastUserData broadcastUserData
flip finally disconnectWsClient $ forever $ do withCleanUp $ forever $ do
handleWSAction handleWSAction
broadcastUserData broadcastUserData
handleWSAction :: handleWSAction ::
( HasWSEnv env, ( MonadWebSocketSession m,
MonadReader env m,
MonadWebSocket m,
MonadConnectedClientsModify m MonadConnectedClientsModify m
) => ) =>
m () m ()
@ -183,27 +48,24 @@ handleWSAction = do
case msg of case msg of
JoinRoomMessage _ -> do JoinRoomMessage _ -> do
joinRoom joinRoom
return ()
ClientInfoMessage clientInfo -> do ClientInfoMessage clientInfo -> do
updateClientName clientInfo updateClientName clientInfo
getMessage :: joinRoom ::
( HasWSEnv env, ( MonadConnectedClientsModify m,
MonadIO m, MonadWebSocketSession m
MonadReader env m
) => ) =>
m Text
getMessage = do
conn' <- getConn <$> ask
liftIO $ WS.receiveData conn'
sendMessage ::
( HasWSEnv env,
MonadIO m,
MonadReader env m
) =>
Text ->
m () m ()
sendMessage msg = do joinRoom = do
conn' <- getConn <$> ask clientId <- getSesssionId
liftIO $ WS.sendTextData conn' msg updateWSClient clientId (\c -> c {joinedRoom = True})
updateClientName ::
( MonadWebSocketSession m,
MonadConnectedClientsModify m
) =>
SetClientInfo ->
m ()
updateClientName clientInfo = do
clientId <- getSesssionId
updateWSClient clientId (\c -> c {name = displayName clientInfo})

View File

@ -0,0 +1,30 @@
module WebSocket.Messages
( getMessage,
sendMessage,
)
where
import ClassyPrelude
import Network.WebSockets qualified as WS
import WebSocket.WSReaderTApp
getMessage ::
( HasWSEnv env,
MonadIO m,
MonadReader env m
) =>
m Text
getMessage = do
conn' <- getConn <$> ask
liftIO $ WS.receiveData conn'
sendMessage ::
( HasWSEnv env,
MonadIO m,
MonadReader env m
) =>
Text ->
m ()
sendMessage msg = do
conn' <- getConn <$> ask
liftIO $ WS.sendTextData conn' msg

View File

@ -0,0 +1,64 @@
{-# LANGUAGE FlexibleInstances #-}
module WebSocket.MonadWebSocketSession
( MonadWebSocketSession (..),
MonadWebSocketSessionInit (..),
)
where
import ClassyPrelude
import Data.Aeson
( FromJSON,
eitherDecodeStrict,
)
import Data.UUID (UUID)
import State.ConnectedClientsState
( MonadConnectedClientsModify,
removeWSClient,
)
import Types.ConnectionState (Client (..))
import Types.WebSocketMessages.WebSocketMessages (SetClientInfo (..))
import WebSocket.Messages
import WebSocket.WSReaderTApp
class Monad m => MonadWebSocketSession m where
getTypedWSMessage :: FromJSON a => m a
getSesssionId :: m UUID
instance MonadWebSocketSession (WSApp WSEnv) where
getTypedWSMessage = do
msg <- getMessage
case eitherDecodeStrict $ encodeUtf8 msg of
Right a -> return a
Left err -> do
sendMessage $ "Bad message: " <> pack err
getTypedWSMessage
getSesssionId = getClientId <$> ask
class (Monad m) => MonadWebSocketSessionInit m where
newClient :: SetClientInfo -> m Client
withCleanUp :: m () -> m ()
instance MonadWebSocketSessionInit (WSApp WSEnv) where
newClient = newClientGeneric
withCleanUp = flip finally disconnectWsClient
newClientGeneric ::
( MonadIO m,
HasWSEnv env,
MonadReader env m
) =>
SetClientInfo ->
m Client
newClientGeneric clientInfo = do
env <- ask
return $ Client {uuid = getClientId env, name = displayName clientInfo, conn = getConn env, joinedRoom = False}
disconnectWsClient ::
( MonadConnectedClientsModify m,
MonadWebSocketSession m
) =>
m ()
disconnectWsClient = do
clientId <- getSesssionId
removeWSClient clientId

View File

@ -0,0 +1,39 @@
{-# LANGUAGE FlexibleContexts #-}
module WebSocket.Server
( runWebSocketServer,
runWSApp,
)
where
import ClassyPrelude
import Data.UUID.V4 (nextRandom)
import Network.WebSockets qualified as WS
import Types.AppTypes
import WebSocket
import WebSocket (WSApp (..))
runWebSocketServer ::
( MonadIO m,
MonadReader Env m
) =>
m ()
runWebSocketServer = do
putStrLn "Websocket up at 127.0.0.1:9160"
wsApp' <- runWSApp
liftIO $ WS.runServer "127.0.0.1" 9160 wsApp'
runWSApp ::
( MonadIO m,
MonadReader Env m
) =>
m WS.ServerApp
runWSApp = do
env <- ask
return
( \pending -> do
conn <- WS.acceptRequest pending
uuid <- nextRandom
let wsEnv = WSEnv {appEnv = env, connection = conn, clientId = uuid}
WS.withPingThread conn 30 (return ()) $ unWSApp wsApp wsEnv
)

View File

@ -0,0 +1,74 @@
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
module WebSocket.WSReaderTApp
( WSApp (..),
WSEnv (..),
HasWSEnv (..),
)
where
import BroadcastUserData
( MonadBroadcast (..),
broadCastToClientsGeneric,
)
import ClassyPrelude
import Data.UUID
import Network.WebSockets qualified as WS
import State.ConnectedClientsState
( MonadConnectedClientsModify (..),
MonadConnectedClientsRead (..),
addWSClientGeneric,
getConnctedClientsGeneric,
removeWSClientGeneric,
updateWSClientGeneric,
)
import State.RoomDataState
import Types.AppTypes
import Types.RoomsState (HasRoomsState (..), getRoomState)
data WSEnv = WSEnv
{ appEnv :: Env,
connection :: WS.Connection,
clientId :: UUID
}
class HasWSEnv a where
getConn :: a -> WS.Connection
getClientId :: a -> UUID
instance HasWSEnv WSEnv where
getConn = connection
getClientId = clientId
newtype WSApp env a = WSApp {unWSApp :: env -> IO a}
deriving
( Functor,
Applicative,
Monad,
MonadReader env,
MonadIO,
MonadUnliftIO
)
via ReaderT env IO
instance HasConnectedClientState WSEnv where
getConnectedClientState = connectedClientsState . appEnv
instance HasRoomsState WSEnv where
getRoomsState = roomsState . appEnv
instance MonadConnectedClientsModify (WSApp WSEnv) where
addWSClient = addWSClientGeneric
updateWSClient = updateWSClientGeneric
removeWSClient = removeWSClientGeneric
instance MonadConnectedClientsRead (WSApp WSEnv) where
getConnctedClients = getConnctedClientsGeneric
instance MonadRoomDataStateRead (WSApp WSEnv) where
getRoomDataState = getRoomState
instance MonadBroadcast (WSApp WSEnv) where
broadCastToClients = broadCastToClientsGeneric