280 lines
8.7 KiB
Haskell
280 lines
8.7 KiB
Haskell
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
module Yesod.WebSockets
|
|
( -- * Core API
|
|
WebSocketsT
|
|
, webSockets
|
|
, webSocketsWith
|
|
, webSocketsOptions
|
|
, webSocketsOptionsWith
|
|
, receiveData
|
|
, receiveDataE
|
|
, receiveDataMessageE
|
|
, sendPing
|
|
, sendPingE
|
|
, sendClose
|
|
, sendCloseE
|
|
, sendTextData
|
|
, sendTextDataE
|
|
, sendBinaryData
|
|
, sendBinaryDataE
|
|
, sendDataMessageE
|
|
-- * Conduit API
|
|
, sourceWS
|
|
, sinkWSText
|
|
, sinkWSBinary
|
|
-- * Async helpers
|
|
, race
|
|
, race_
|
|
, concurrently
|
|
, concurrently_
|
|
-- * Re-exports from websockets
|
|
, WS.defaultConnectionOptions
|
|
, WS.ConnectionOptions (..)
|
|
) where
|
|
|
|
import Control.Monad (forever, when)
|
|
import Control.Monad.Reader (ReaderT, runReaderT, MonadReader, ask)
|
|
import Conduit
|
|
import qualified Network.Wai.Handler.WebSockets as WaiWS
|
|
import qualified Network.WebSockets as WS
|
|
import qualified Yesod.Core as Y
|
|
import UnliftIO (SomeException, tryAny, MonadIO, liftIO, MonadUnliftIO, withRunInIO, race, race_, concurrently, concurrently_)
|
|
|
|
-- | A transformer for a WebSockets handler.
|
|
--
|
|
-- Since 0.1.0
|
|
type WebSocketsT = ReaderT WS.Connection
|
|
|
|
-- | Attempt to run a WebSockets handler. This function first checks if the
|
|
-- client initiated a WebSockets connection and, if so, runs the provided
|
|
-- application, short-circuiting the rest of your handler. If the client did
|
|
-- not request a WebSockets connection, the rest of your handler will be called
|
|
-- instead.
|
|
--
|
|
-- Since 0.1.0
|
|
webSockets
|
|
:: (MonadUnliftIO m, Y.MonadHandler m)
|
|
=> WebSocketsT m ()
|
|
-> m ()
|
|
webSockets = webSocketsOptions WS.defaultConnectionOptions
|
|
|
|
-- | Varient of 'webSockets' which allows you to specify
|
|
-- the WS.ConnectionOptions setttings when upgrading to a websocket connection.
|
|
--
|
|
-- Since 0.2.5
|
|
webSocketsOptions
|
|
:: (MonadUnliftIO m, Y.MonadHandler m)
|
|
=> WS.ConnectionOptions
|
|
-> WebSocketsT m ()
|
|
-> m ()
|
|
webSocketsOptions opts = webSocketsOptionsWith opts $ const $ return $ Just $ WS.AcceptRequest Nothing []
|
|
|
|
-- | Varient of 'webSockets' which allows you to specify the 'WS.AcceptRequest'
|
|
-- setttings when upgrading to a websocket connection.
|
|
--
|
|
-- Since 0.2.4
|
|
webSocketsWith :: (MonadUnliftIO m, Y.MonadHandler m)
|
|
=> (WS.RequestHead -> m (Maybe WS.AcceptRequest))
|
|
-- ^ A Nothing indicates that the websocket upgrade request should not happen
|
|
-- and instead the rest of the handler will be called instead. This allows
|
|
-- you to use 'WS.getRequestSubprotocols' and only accept the request if
|
|
-- a compatible subprotocol is given. Also, the action runs before upgrading
|
|
-- the request to websockets, so you can also use short-circuiting handler
|
|
-- actions such as 'Y.invalidArgs'.
|
|
-> WebSocketsT m ()
|
|
-> m ()
|
|
webSocketsWith = webSocketsOptionsWith WS.defaultConnectionOptions
|
|
|
|
-- | Varient of 'webSockets' which allows you to specify both
|
|
-- the WS.ConnectionOptions and the 'WS.AcceptRequest'
|
|
-- setttings when upgrading to a websocket connection.
|
|
--
|
|
-- Since 0.2.5
|
|
webSocketsOptionsWith :: (MonadUnliftIO m, Y.MonadHandler m)
|
|
=> WS.ConnectionOptions
|
|
-- ^ Custom websockets options
|
|
-> (WS.RequestHead -> m (Maybe WS.AcceptRequest))
|
|
-- ^ A Nothing indicates that the websocket upgrade request should not happen
|
|
-- and instead the rest of the handler will be called instead. This allows
|
|
-- you to use 'WS.getRequestSubprotocols' and only accept the request if
|
|
-- a compatible subprotocol is given. Also, the action runs before upgrading
|
|
-- the request to websockets, so you can also use short-circuiting handler
|
|
-- actions such as 'Y.invalidArgs'.
|
|
-> WebSocketsT m ()
|
|
-> m ()
|
|
webSocketsOptionsWith wsConnOpts buildAr inner = do
|
|
req <- Y.waiRequest
|
|
when (WaiWS.isWebSocketsReq req) $ do
|
|
let rhead = WaiWS.getRequestHead req
|
|
mar <- buildAr rhead
|
|
case mar of
|
|
Nothing -> return ()
|
|
Just ar ->
|
|
Y.sendRawResponseNoConduit
|
|
$ \src sink -> withRunInIO $ \runInIO -> WaiWS.runWebSockets
|
|
wsConnOpts
|
|
rhead
|
|
(\pconn -> do
|
|
conn <- WS.acceptRequestWith pconn ar
|
|
WS.forkPingThread conn 30
|
|
runInIO $ runReaderT inner conn)
|
|
src
|
|
sink
|
|
|
|
-- | Wrapper for capturing exceptions
|
|
wrapWSE :: (MonadIO m, MonadReader WS.Connection m)
|
|
=> (WS.Connection -> a -> IO ())
|
|
-> a
|
|
-> m (Either SomeException ())
|
|
wrapWSE ws x = do
|
|
conn <- ask
|
|
liftIO $ tryAny $ ws conn x
|
|
|
|
wrapWS :: (MonadIO m, MonadReader WS.Connection m)
|
|
=> (WS.Connection -> a -> IO ())
|
|
-> a
|
|
-> m ()
|
|
wrapWS ws x = do
|
|
conn <- ask
|
|
liftIO $ ws conn x
|
|
|
|
-- | Receive a piece of data from the client.
|
|
--
|
|
-- Since 0.1.0
|
|
receiveData
|
|
:: (MonadIO m, MonadReader WS.Connection m, WS.WebSocketsData a)
|
|
=> m a
|
|
receiveData = do
|
|
conn <- ask
|
|
liftIO $ WS.receiveData conn
|
|
|
|
-- | Receive a piece of data from the client.
|
|
-- Capture SomeException as the result or operation
|
|
-- Since 0.2.2
|
|
receiveDataE
|
|
:: (MonadIO m, MonadReader WS.Connection m, WS.WebSocketsData a)
|
|
=> m (Either SomeException a)
|
|
receiveDataE = do
|
|
conn <- ask
|
|
liftIO $ tryAny $ WS.receiveData conn
|
|
|
|
-- | Receive an application message.
|
|
-- Capture SomeException as the result or operation
|
|
-- Since 0.2.3
|
|
receiveDataMessageE
|
|
:: (MonadIO m, MonadReader WS.Connection m)
|
|
=> m (Either SomeException WS.DataMessage)
|
|
receiveDataMessageE = do
|
|
conn <- ask
|
|
liftIO $ tryAny $ WS.receiveDataMessage conn
|
|
|
|
-- | Send a textual message to the client.
|
|
--
|
|
-- Since 0.1.0
|
|
sendTextData
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> m ()
|
|
sendTextData = wrapWS WS.sendTextData
|
|
|
|
-- | Send a textual message to the client.
|
|
-- Capture SomeException as the result or operation
|
|
-- and can be used like
|
|
-- `either handle_exception return =<< sendTextDataE ("Welcome" :: Text)`
|
|
-- Since 0.2.2
|
|
sendTextDataE
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> m (Either SomeException ())
|
|
sendTextDataE = wrapWSE WS.sendTextData
|
|
|
|
-- | Send a binary message to the client.
|
|
--
|
|
-- Since 0.1.0
|
|
sendBinaryData
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> m ()
|
|
sendBinaryData = wrapWS WS.sendBinaryData
|
|
|
|
-- | Send a binary message to the client.
|
|
-- Capture SomeException as the result of operation
|
|
-- Since 0.2.2
|
|
sendBinaryDataE
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> m (Either SomeException ())
|
|
sendBinaryDataE = wrapWSE WS.sendBinaryData
|
|
|
|
-- | Send a ping message to the client.
|
|
--
|
|
-- Since 0.2.2
|
|
sendPing
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> WebSocketsT m ()
|
|
sendPing = wrapWS WS.sendPing
|
|
|
|
-- | Send a ping message to the client.
|
|
-- Capture SomeException as the result of operation
|
|
-- Since 0.2.2
|
|
sendPingE
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> m (Either SomeException ())
|
|
sendPingE = wrapWSE WS.sendPing
|
|
|
|
-- | Send a DataMessage to the client.
|
|
-- Capture SomeException as the result of operation
|
|
-- Since 0.2.3
|
|
sendDataMessageE
|
|
:: (MonadIO m, MonadReader WS.Connection m)
|
|
=> WS.DataMessage
|
|
-> m (Either SomeException ())
|
|
sendDataMessageE x = do
|
|
conn <- ask
|
|
liftIO $ tryAny $ WS.sendDataMessage conn x
|
|
|
|
-- | Send a close request to the client.
|
|
--
|
|
-- Since 0.2.2
|
|
sendClose
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> WebSocketsT m ()
|
|
sendClose = wrapWS WS.sendClose
|
|
|
|
-- | Send a close request to the client.
|
|
-- Capture SomeException as the result of operation
|
|
-- Since 0.2.2
|
|
sendCloseE
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> a
|
|
-> m (Either SomeException ())
|
|
sendCloseE = wrapWSE WS.sendClose
|
|
|
|
-- | A @Source@ of WebSockets data from the user.
|
|
--
|
|
-- Since 0.1.0
|
|
sourceWS
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> ConduitT i a m ()
|
|
sourceWS = forever $ lift receiveData >>= yield
|
|
|
|
-- | A @Sink@ for sending textual data to the user.
|
|
--
|
|
-- Since 0.1.0
|
|
sinkWSText
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> ConduitT a o m ()
|
|
sinkWSText = mapM_C sendTextData
|
|
|
|
-- | A @Sink@ for sending binary data to the user.
|
|
--
|
|
-- Since 0.1.0
|
|
sinkWSBinary
|
|
:: (MonadIO m, WS.WebSocketsData a, MonadReader WS.Connection m)
|
|
=> ConduitT a o m ()
|
|
sinkWSBinary = mapM_C sendBinaryData
|