{-# 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();})"