haste-lib-0.6.0.0: src/Haste/WebSockets.hs
{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, FlexibleInstances #-}
-- | WebSockets API for Haste.
module Haste.WebSockets
( module Haste.Concurrent
, WebSocket, WebSocketConfig (..), WebSocketCloseEvent (..)
, WebSocketData (..)
, noHandlers
, wsOpenSync
, wsClose
, withWebSocket, withBinaryWebSocket, wsSendBlob
) where
import Haste
import Haste.Prim.Foreign
import Haste.Concurrent
import Haste.Binary (Blob)
-- | A WebSocket, parameterized over the type of data that can be sent and
-- received over it.
newtype WebSocket a = WebSocket JSAny deriving (ToAny, FromAny)
instance Eq (WebSocket a) where
(WebSocket a) == (WebSocket b) = a == b
-- | Some data that can be sent and received over a WebSocket.
class WebSocketData a where
-- | Send a piece of data to a WebSocket of the appropriate type.
-- Returns @False@ if the data could not be sent, for instance due to a
-- closed socket.
wsSend :: WebSocket a -> a -> CIO Bool
-- | Open a new WebSocket connection, returning @Just socket@ when successful.
wsOpen :: WebSocketConfig a -> CIO (Maybe (WebSocket a))
instance WebSocketData Blob where
wsSend ws b = liftIO $ sendB ws b
wsOpen = open newBin
instance WebSocketData JSString where
wsSend ws s = liftIO $ sendS ws s
wsOpen = open new
open mkNew (WebSocketConfig url close msg) = do
sock <- newEmptyMVar
liftIO $ mkNew url
(\s d -> concurrent $ msg s d)
(concurrent . putMVar sock . Just)
(concurrent . close)
(concurrent $ putMVar sock Nothing)
takeMVar sock
-- | The various event handlers supported by WebSockets.
data WebSocketConfig t = WebSocketConfig
{ -- | URL to which to attempt a WebSocket connection.
wsOpenURL :: URL
-- | Called when the connection is closed by the server.
, wsOnClose :: WebSocketCloseEvent -> CIO ()
-- | Called when a new message arrives.
, wsOnMessage :: WebSocket t -> t -> CIO ()
}
-- | Event describing the remote closing of a WebSocket.
data WebSocketCloseEvent = WebSocketCloseEvent
{ -- | Reason for close event, computer-readable version.
wsCloseCode :: Int
-- | Reason for close event, human-readable version.
, wsCloseReason :: JSString
-- | Was the socket closed cleanly?
, wsCloseClean :: Bool
}
instance FromAny WebSocketCloseEvent where
fromAny o = WebSocketCloseEvent <$> get o "code"
<*> get o "reason"
<*> get o "wasClean"
-- | No-op handlers for all WebSocket events.
noHandlers :: WebSocketConfig t
noHandlers = WebSocketConfig
{ wsOpenURL = ""
, wsOnClose = \_ -> return ()
, wsOnMessage = \_ _ -> return ()
}
-- | Open a WebSocket, returning the socket as well as a blocking CIO
-- computation to await incoming data on the socket.
-- When data arrives on the socket, the waiting computation will return
-- @Right received_data@. When the socket is closed, the waiting computation
-- will return @Left close_event@.
wsOpenSync :: WebSocketData t
=> URL
-> CIO (Maybe (WebSocket t, CIO (Either WebSocketCloseEvent t)))
wsOpenSync url = do
incoming <- newEmptyMVar
msock <- wsOpen (WebSocketConfig
{ wsOpenURL = url
, wsOnClose = putMVar incoming . Left
, wsOnMessage = \_ -> putMVar incoming . Right
})
case msock of
Just sock -> return $ Just (sock, takeMVar incoming)
_ -> return Nothing
-- | Close a WebSocket. Closing a WebSocket which has been previously closed
-- is a no-op. Any associated 'wsOnClose' handler will not be called.
wsClose :: WebSocket a -> CIO ()
wsClose ws = liftIO $ close ws
-- * Legacy API
-- | Run a computation with a WebSocket. The computation will not be executed
-- until a connection to the server has been established.
-- The WebSocket will not be closed after the computation finishes.
withWebSocket :: URL
-- ^ URL to bind the WebSocket to
-> (WebSocket JSString -> JSString -> CIO ())
-- ^ Computation to run when new data arrives
-> CIO a
-- ^ Computation to run when an error occurs
-> (WebSocket JSString -> CIO a)
-- ^ Computation using the WebSocket
-> CIO a
withWebSocket = withWS
-- | Run a computation with a WebSocket. The computation will not be executed
-- until a connection to the server has been established.
-- The WebSocket will not be closed after the computation finishes.
withBinaryWebSocket :: URL
-- ^ URL to bind the WebSocket to
-> (WebSocket Blob -> Blob -> CIO ())
-- ^ Computation to run when new data arrives
-> CIO a
-- ^ Computation to run when an error occurs
-> (WebSocket Blob -> CIO a)
-- ^ Computation using the WebSocket
-> CIO a
withBinaryWebSocket = withWS
-- | Worker for legacy API.
withWS :: WebSocketData t
=> URL
-> (WebSocket t -> t -> CIO ())
-> CIO a
-> (WebSocket t -> CIO a)
-> CIO a
withWS url cb err f = do
mws <- wsOpen (noHandlers {wsOpenURL = url, wsOnMessage = cb})
case mws of
Just ws -> f ws
_ -> err
-- | Send a Blob over a WebSocket.
wsSendBlob :: WebSocket Blob -> Blob -> CIO Bool
wsSendBlob = wsSend
-- * Hairy internals
-- | Open a new WebSocket in text mode.
new :: URL
-> (WebSocket JSString -> JSString -> IO ())
-> (WebSocket JSString -> IO ())
-> (WebSocketCloseEvent -> IO ())
-> IO ()
-> IO ()
new = ffi "(function(url, cb, f, close, err) {\
\var ws = new WebSocket(url);\
\ws.onmessage = function(e) {cb(ws,e.data);};\
\ws.onopen = function(e) {f(ws);};\
\ws.onclose = function(e) {close(e.data);};\
\ws.error = function(e) {err();};\
\return ws;\
\})"
-- | Open a new WebSocket in binary mode.
newBin :: URL
-> (WebSocket Blob -> Blob -> IO ())
-> (WebSocket Blob -> IO ())
-> (WebSocketCloseEvent -> IO ())
-> IO ()
-> IO ()
newBin = ffi "(function(url, cb, f, close, err) {\
\var ws = new WebSocket(url);\
\ws.binaryType = 'blob';\
\ws.onmessage = function(e) {cb(ws,e.data);};\
\ws.onopen = function(e) {f(ws);};\
\ws.onclose = function(e) {close(e.data);};\
\ws.onerror = function(e) {err();};\
\return ws;\
\})"
-- | Send text over a WebSocket.
sendS :: WebSocket JSString -> JSString -> IO Bool
sendS = ffi "(function(s, msg) {if(s.readyState != 1) {return false;} else {s.send(msg); return true;}})"
-- | Send a blob over a WebSocket.
sendB :: WebSocket Blob -> Blob -> IO Bool
sendB = ffi "(function(s, msg) {if(s.readyState != 1) {return false;} else {s.send(msg); return true;}})"
-- | Close a WebSocket.
close :: WebSocket a -> IO ()
close = ffi "(function(ws){ws.onclose = ws.onerror = function(){}; ws.close();})"