websockets-rpc-0.0.0: src/Network/WebSockets/RPC.hs
{-# LANGUAGE
RankNTypes
, ScopedTypeVariables
, NamedFieldPuns
#-}
module Network.WebSockets.RPC
( -- * Server
RPCServerParams (..), RPCServer, rpcServer
, -- * Client
RPCClientParams (..), RPCClient (..), rpcClient
, -- * Re-Exports
WebSocketServerRPCT, execWebSocketServerRPCT, WebSocketClientRPCT, execWebSocketClientRPCT
, WebSocketRPCException (..)
) where
import Network.WebSockets.RPC.Trans.Server ( WebSocketServerRPCT, runWebSocketServerRPCT', getServerEnv, execWebSocketServerRPCT
, registerSubscribeSupply, runSubscribeSupply, unregisterSubscribeSupply)
import Network.WebSockets.RPC.Trans.Client ( WebSocketClientRPCT, runWebSocketClientRPCT', getClientEnv, execWebSocketClientRPCT
, registerReplyComplete, runReply, runComplete, unregisterReplyComplete
, freshRPCID
)
import Network.WebSockets.RPC.Types ( WebSocketRPCException (..), Subscribe (..), Supply (..), Reply (..), Complete (..)
, ClientToServer (Sub, Sup), ServerToClient (Rep, Com)
, RPCIdentified (..)
)
import Network.WebSockets (acceptRequest, receiveDataMessage, sendDataMessage, DataMessage (Text, Binary))
import Network.Wai.Trans (ServerAppT, ClientAppT)
import Data.Aeson (ToJSON, FromJSON, decode, encode)
import Control.Monad (forever)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Control.Monad.Catch (MonadThrow, throwM)
-- * Server
data RPCServerParams rep com m = RPCServerParams
{ reply :: rep -> m () -- ^ dispatch a reply
, complete :: com -> m () -- ^ dispatch a onComplete
}
type RPCServer sub sup rep com m
= RPCServerParams rep com m
-> Either sub sup -- ^ handle incoming message
-> m ()
-- | May throw a 'WebSocketRPCParseFailure' if the opposing party sends bad data
rpcServer :: forall sub sup rep com m
. ( ToJSON rep
, ToJSON com
, FromJSON sub
, FromJSON sup
, MonadIO m
, MonadThrow m
)
=> RPCServer sub sup rep com m
-> ServerAppT (WebSocketServerRPCT sub sup m)
rpcServer f pendingConn = do
conn <- liftIO (acceptRequest pendingConn)
let runSub :: Subscribe sub -> WebSocketServerRPCT sub sup m ()
runSub (Subscribe RPCIdentified{_ident,_params}) = do
env <- getServerEnv
let reply :: rep -> m ()
reply rep =
let r = Reply RPCIdentified{_ident, _params = rep}
in liftIO (sendDataMessage conn (Text (encode r)))
complete :: com -> m ()
complete com =
let c = Complete RPCIdentified{_ident, _params = com}
in do liftIO (sendDataMessage conn (Text (encode c)))
runWebSocketServerRPCT' env (unregisterSubscribeSupply _ident)
cont :: Either sub sup -> m ()
cont = f RPCServerParams{reply,complete}
registerSubscribeSupply _ident cont
runSubscribeSupply _ident (Left _params)
runSup :: Supply sup -> WebSocketServerRPCT sub sup m ()
runSup (Supply RPCIdentified{_ident,_params}) =
case _params of
Nothing -> unregisterSubscribeSupply _ident -- FIXME this could bork the server if I `async` a routine thread
Just params -> runSubscribeSupply _ident (Right params)
forever $ do
data' <- liftIO (receiveDataMessage conn)
case data' of
Text xs -> case decode xs of
Nothing ->
throwM (WebSocketRPCParseFailure ["server","text"] xs)
Just x -> case x of
Sub sub -> runSub sub
Sup sup -> runSup sup
Binary xs -> case decode xs of
Nothing ->
throwM (WebSocketRPCParseFailure ["server","binary"] xs)
Just x -> case x of
Sub sub -> runSub sub
Sup sup -> runSup sup
-- * Client
data RPCClientParams sup m = RPCClientParams
{ supply :: sup -> m () -- ^ dispatch a supply
, cancel :: m () -- ^ cancel the RPC call
}
data RPCClient sub sup rep com m = RPCClient
{ subscription :: !sub
, onReply :: RPCClientParams sup m
-> rep
-> m () -- ^ handle incoming reply
, onComplete :: com -> m () -- ^ handle finalized onComplete
}
-- | May throw a 'WebSocketRPCParseFailure' if the opposing party sends bad data
rpcClient :: forall sub sup rep com m
. ( ToJSON sub
, ToJSON sup
, FromJSON rep
, FromJSON com
, MonadIO m
, MonadThrow m
)
=> ((RPCClient sub sup rep com m -> WebSocketClientRPCT rep com m ()) -> WebSocketClientRPCT rep com m ())
-> ClientAppT (WebSocketClientRPCT rep com m) ()
rpcClient userGo conn =
let go RPCClient{subscription,onReply,onComplete} = do
_ident <- freshRPCID
liftIO (sendDataMessage conn (Text (encode (Subscribe RPCIdentified{_ident, _params = subscription}))))
env <- getClientEnv
let supply :: sup -> m ()
supply sup = liftIO (sendDataMessage conn (Text (encode (Supply RPCIdentified{_ident, _params = Just sup}))))
cancel :: m ()
cancel = do
liftIO (sendDataMessage conn (Text (encode (Supply RPCIdentified{_ident, _params = Nothing :: Maybe ()}))))
runWebSocketClientRPCT' env (unregisterReplyComplete _ident)
registerReplyComplete _ident (onReply RPCClientParams{supply,cancel}) onComplete
let runRep :: Reply rep -> WebSocketClientRPCT rep com m ()
runRep (Reply RPCIdentified{_ident = _ident',_params})
| _ident' == _ident = runReply _ident _params
| otherwise = pure () -- FIXME fail somehow legibly??
runCom :: Complete com -> WebSocketClientRPCT rep com m ()
runCom (Complete RPCIdentified{_ident = _ident', _params})
| _ident' == _ident = do
runComplete _ident' _params -- NOTE don't use onComplete here because it might not exist later
unregisterReplyComplete _ident'
| otherwise = pure ()
forever $ do
data' <- liftIO (receiveDataMessage conn)
case data' of
Text xs ->
case decode xs of
Nothing ->
throwM (WebSocketRPCParseFailure ["client","text"] xs)
Just x -> case x of
Rep rep -> runRep rep
Com com -> runCom com
Binary xs ->
case decode xs of
Nothing ->
throwM (WebSocketRPCParseFailure ["client","binary"] xs)
Just x -> case x of
Rep rep -> runRep rep
Com com -> runCom com
in userGo go