From 865b69e7998c88e708e9e7f1951371e0c0b51b2b Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 19 Feb 2023 11:41:32 +0100 Subject: [PATCH] Pure WS monad --- backend/src/BroadcastUserData.hs | 59 +++-- backend/src/Lib.hs | 2 +- backend/src/RoomDataHandler.hs | 19 +- backend/src/State/ConnectedClientsState.hs | 2 +- backend/src/State/RoomDataState.hs | 14 ++ backend/src/Types/RoomsState.hs | 15 +- backend/src/WebServer.hs | 43 +++- backend/src/WebSocket.hs | 206 +++--------------- backend/src/WebSocket/Messages.hs | 30 +++ .../src/WebSocket/MonadWebSocketSession.hs | 64 ++++++ backend/src/WebSocket/Server.hs | 39 ++++ backend/src/WebSocket/WSReaderTApp.hs | 74 +++++++ 12 files changed, 346 insertions(+), 221 deletions(-) create mode 100644 backend/src/State/RoomDataState.hs create mode 100644 backend/src/WebSocket/Messages.hs create mode 100644 backend/src/WebSocket/MonadWebSocketSession.hs create mode 100644 backend/src/WebSocket/Server.hs create mode 100644 backend/src/WebSocket/WSReaderTApp.hs diff --git a/backend/src/BroadcastUserData.hs b/backend/src/BroadcastUserData.hs index 629e37b..b1a0a54 100644 --- a/backend/src/BroadcastUserData.hs +++ b/backend/src/BroadcastUserData.hs @@ -1,52 +1,51 @@ module BroadcastUserData ( broadcastUserData, + broadCastToClientsGeneric, + MonadBroadcast (..), ) where -import ClassyPrelude -import Data.Aeson (encode) -import Network.WebSockets qualified as WS -import Types.AppTypes (HasConnectedClientState (..)) -import Types.ConnectionState (Client (..), ConnectedClients) -import Types.RoomsState (HasRoomsState (..)) -import Types.UsersData (UsersData (..)) +import ClassyPrelude +import Data.Aeson (encode) +import qualified Network.WebSockets as WS +import State.ConnectedClientsState (MonadConnectedClientsRead (getConnctedClients)) +import State.RoomDataState (MonadRoomDataStateRead (getRoomDataState)) +import Types.AppTypes (HasConnectedClientState (..)) +import Types.ConnectionState (Client (..), ConnectedClients) +import Types.RoomsState (HasRoomsState (..)) +import Types.UsersData (UsersData (..)) + +class (Monad m, MonadConnectedClientsRead m) => MonadBroadcast m where + broadCastToClients :: Text -> m () broadcastUserData :: - ( MonadIO m, - HasConnectedClientState env, - HasRoomsState env, - MonadReader env m + ( MonadRoomDataStateRead m, + MonadBroadcast m ) => m () broadcastUserData = do - putStrLn "broadcasting" userWithOutRoom <- getUsersWithoutRoom - roomsData <- ask >>= readMVar . getRoomsState + roomsData <- getRoomDataState let usersData = UsersData {usersWithOutRoom = userWithOutRoom, roomsData = roomsData} broadCastToClients $ (decodeUtf8 . toStrict . encode) usersData getUsersWithoutRoom :: - ( MonadIO m, - HasConnectedClientState env, - MonadReader env m + ( MonadConnectedClientsRead m ) => m [Text] -getUsersWithoutRoom = do - state <- ask >>= readMVar . getConnectedClientState - return $ map name $ filter (not . joinedRoom) state +getUsersWithoutRoom = map name . filter (not . joinedRoom) <$> getConnctedClients + +broadCastToClientsGeneric :: + ( MonadIO m, + MonadConnectedClientsRead m + ) => + Text -> + m () +broadCastToClientsGeneric message = do + state <- getConnctedClients + liftIO $ broadcast message state broadcast :: Text -> ConnectedClients -> IO () broadcast message clients = do putStrLn 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 diff --git a/backend/src/Lib.hs b/backend/src/Lib.hs index 9945c0a..b468739 100644 --- a/backend/src/Lib.hs +++ b/backend/src/Lib.hs @@ -10,7 +10,7 @@ import Types.AppTypes import Types.ConnectionState (initConnectionsState) import Types.RoomsState (initRoomsState) import WebServer (runWebServer) -import WebSocket (runWebSocketServer) +import WebSocket.Server (runWebSocketServer) runBothServers :: IO () runBothServers = do diff --git a/backend/src/RoomDataHandler.hs b/backend/src/RoomDataHandler.hs index 2fd2732..a2e7757 100644 --- a/backend/src/RoomDataHandler.hs +++ b/backend/src/RoomDataHandler.hs @@ -5,7 +5,7 @@ module RoomDataHandler ) where -import BroadcastUserData (broadcastUserData) +import BroadcastUserData (MonadBroadcast, broadcastUserData) import ClassyPrelude import Control.Monad.Except (MonadError, throwError) import Data.Aeson (eitherDecodeStrict) @@ -16,27 +16,36 @@ import Network.Wai consumeRequestBodyStrict, responseLBS, ) +import State.ConnectedClientsState (MonadConnectedClientsRead) +import State.RoomDataState + ( MonadRoomDataStateModify (setRoomDataState), + MonadRoomDataStateRead, + ) import Types.AppTypes (HasConnectedClientState) import Types.RoomsState ( HasRoomsState, roomStateDiffers, updateRoomState, ) -import Types.WebEnv (HasWebEnv (getRequest), getRespond) +import Types.WebEnv + ( HasWebEnv (getRequest), + getRespond, + ) roomDataHandler :: ( MonadIO m, HasWebEnv env, - HasConnectedClientState env, MonadReader env m, MonadError ResponseReceived m, - HasRoomsState env + MonadRoomDataStateRead m, + MonadRoomDataStateModify m, + MonadBroadcast m ) => m ResponseReceived roomDataHandler = do newRoomData <- parseBodyOrBadRequest whenM (roomStateDiffers newRoomData) $ do - updateRoomState newRoomData + setRoomDataState newRoomData broadcastUserData success diff --git a/backend/src/State/ConnectedClientsState.hs b/backend/src/State/ConnectedClientsState.hs index acca4a7..e2ff726 100644 --- a/backend/src/State/ConnectedClientsState.hs +++ b/backend/src/State/ConnectedClientsState.hs @@ -82,7 +82,7 @@ class Monad m => MonadConnectedClientsRead m where getConnctedClientsGeneric :: ( HasConnectedClientState env, MonadReader env m, - MonadUnliftIO m + MonadIO m ) => m ConnectedClients getConnctedClientsGeneric = do diff --git a/backend/src/State/RoomDataState.hs b/backend/src/State/RoomDataState.hs new file mode 100644 index 0000000..35140df --- /dev/null +++ b/backend/src/State/RoomDataState.hs @@ -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 diff --git a/backend/src/Types/RoomsState.hs b/backend/src/Types/RoomsState.hs index 3d3fe98..df5ae71 100644 --- a/backend/src/Types/RoomsState.hs +++ b/backend/src/Types/RoomsState.hs @@ -4,10 +4,12 @@ module Types.RoomsState HasRoomsState (..), roomStateDiffers, updateRoomState, + getRoomState, ) where import ClassyPrelude +import State.RoomDataState (MonadRoomDataStateRead (getRoomDataState)) import Types.RoomData (RoomsData) type RoomsState = MVar RoomsData @@ -30,16 +32,23 @@ updateRoomState newData = do _ <- swapMVar state newData return () -roomStateDiffers :: +getRoomState :: ( HasRoomsState env, MonadIO m, MonadReader env m ) => + m RoomsData +getRoomState = do + state <- getRoomsState <$> ask + readMVar state + +roomStateDiffers :: + ( MonadRoomDataStateRead m + ) => RoomsData -> m Bool roomStateDiffers newData = do - currentData <- ask >>= readMVar . getRoomsState - return $ not $ eqIgnoreOrdering newData currentData + not . eqIgnoreOrdering newData <$> getRoomDataState eqIgnoreOrdering :: (Eq a) => [a] -> [a] -> Bool eqIgnoreOrdering a b = length a == length b && all (`elem` b) a diff --git a/backend/src/WebServer.hs b/backend/src/WebServer.hs index 55abc0e..c32a14d 100644 --- a/backend/src/WebServer.hs +++ b/backend/src/WebServer.hs @@ -1,22 +1,33 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} module WebServer (runWebServer) where +import BroadcastUserData + ( MonadBroadcast (..), + broadCastToClientsGeneric, + ) import ClassyPrelude hiding (decodeUtf8) import Control.Monad.Except import Network.HTTP.Types import Network.Wai import Network.Wai.Handler.Warp (run) import RoomDataHandler (roomDataHandler) -import Types.AppTypes - ( Env (..), - HasConnectedClientState (..), +import State.ConnectedClientsState + ( MonadConnectedClientsRead (..), + getConnctedClientsGeneric, ) +import State.RoomDataState + ( MonadRoomDataStateModify (..), + MonadRoomDataStateRead (getRoomDataState), + ) +import Types.AppTypes (Env (..)) import Types.RoomsState ( HasRoomsState (getRoomsState), + getRoomState, roomStateDiffers, updateRoomState, ) @@ -43,6 +54,18 @@ newtype WebApp env a = WebApp {unWebApp :: env -> IO (Either ResponseReceived a) ) 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 :: ( MonadIO m, HasWebEnv env, @@ -56,10 +79,11 @@ getRequestPath = do app :: ( MonadIO m, HasWebEnv env, - HasConnectedClientState env, MonadReader env m, - HasRoomsState env, - MonadError ResponseReceived m + MonadError ResponseReceived m, + MonadRoomDataStateModify m, + MonadRoomDataStateRead m, + MonadBroadcast m ) => m ResponseReceived app = requestPathHandler @@ -67,10 +91,11 @@ app = requestPathHandler requestPathHandler :: ( MonadIO m, HasWebEnv env, - HasConnectedClientState env, MonadReader env m, - HasRoomsState env, - MonadError ResponseReceived m + MonadError ResponseReceived m, + MonadRoomDataStateModify m, + MonadRoomDataStateRead m, + MonadBroadcast m ) => m ResponseReceived requestPathHandler = do diff --git a/backend/src/WebSocket.hs b/backend/src/WebSocket.hs index 455ad16..e0c047a 100644 --- a/backend/src/WebSocket.hs +++ b/backend/src/WebSocket.hs @@ -3,178 +3,43 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-} -module WebSocket (runWebSocketServer) where +module WebSocket (WSEnv (..), wsApp, WSApp (..)) where -import BroadcastUserData (broadcastUserData) +import BroadcastUserData + ( MonadBroadcast (..), + broadcastUserData, + ) import ClassyPrelude -import Data.Aeson -import Data.UUID (UUID) -import Data.UUID.V4 (nextRandom) -import Network.WebSockets qualified as WS -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 Data.UUID.V4 () +import State.ConnectedClientsState (MonadConnectedClientsModify (..)) +import State.RoomDataState (MonadRoomDataStateRead (..)) +import Types.ConnectionState (Client (..)) import Types.WebSocketMessages.WebSocketMessages ( SetClientInfo (displayName), WebSocketMessage (..), ) - -data WSEnv = WSEnv - { 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 +import WebSocket.MonadWebSocketSession +import WebSocket.WSReaderTApp wsApp :: - ( HasWSEnv env, - HasConnectedClientState env, - HasRoomsState env, - MonadReader env m, - MonadUnliftIO m, - MonadWebSocket m, - MonadConnectedClientsModify m + ( MonadWebSocketSession m, + MonadWebSocketSessionInit m, + MonadConnectedClientsModify m, + MonadRoomDataStateRead m, + MonadBroadcast m ) => m () wsApp = do msg <- getTypedWSMessage - print msg client <- newClient msg addWSClient client broadcastUserData - flip finally disconnectWsClient $ forever $ do + withCleanUp $ forever $ do handleWSAction broadcastUserData handleWSAction :: - ( HasWSEnv env, - MonadReader env m, - MonadWebSocket m, + ( MonadWebSocketSession m, MonadConnectedClientsModify m ) => m () @@ -183,27 +48,24 @@ handleWSAction = do case msg of JoinRoomMessage _ -> do joinRoom - return () ClientInfoMessage clientInfo -> do updateClientName clientInfo -getMessage :: - ( HasWSEnv env, - MonadIO m, - MonadReader env m +joinRoom :: + ( MonadConnectedClientsModify m, + MonadWebSocketSession 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 +joinRoom = do + clientId <- getSesssionId + 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}) diff --git a/backend/src/WebSocket/Messages.hs b/backend/src/WebSocket/Messages.hs new file mode 100644 index 0000000..b3ec3a6 --- /dev/null +++ b/backend/src/WebSocket/Messages.hs @@ -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 diff --git a/backend/src/WebSocket/MonadWebSocketSession.hs b/backend/src/WebSocket/MonadWebSocketSession.hs new file mode 100644 index 0000000..68b57f6 --- /dev/null +++ b/backend/src/WebSocket/MonadWebSocketSession.hs @@ -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 diff --git a/backend/src/WebSocket/Server.hs b/backend/src/WebSocket/Server.hs new file mode 100644 index 0000000..e8219a0 --- /dev/null +++ b/backend/src/WebSocket/Server.hs @@ -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 + ) diff --git a/backend/src/WebSocket/WSReaderTApp.hs b/backend/src/WebSocket/WSReaderTApp.hs new file mode 100644 index 0000000..3ea6ef3 --- /dev/null +++ b/backend/src/WebSocket/WSReaderTApp.hs @@ -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