on develop help sign in
Add Reflex.Dom.WebSocket
commitddcc75b5cd340fece60482fd77d7d92e09175cea authorAli Abrar <aliabrar@gmail.com> dateTue 17 Nov 2015, 23:56 (11 years ago) parentc4ac902
7 files changed+235−12
reflex-dom.cabal+3−1 src-ghc/Reflex/Dom/Internal/Foreign.hs+32 src-ghc/Reflex/Dom/WebSocket/Foreign.hs+84 src-ghc/Reflex/Dom/Xhr/Foreign.hs+2−11 src-ghcjs/Reflex/Dom/WebSocket/Foreign.hs+49 src/Reflex/Dom.hs+2 src/Reflex/Dom/WebSocket.hs+63
reflex-dom.cabal+3−1
@@ -59,15 +59,17 @@ library
5959 Reflex.Dom
6060 Reflex.Dom.Class
6161 Reflex.Dom.Internal
62+ Reflex.Dom.Time
63+ Reflex.Dom.WebSocket
6264 Reflex.Dom.Widget
6365 Reflex.Dom.Widget.Basic
6466 Reflex.Dom.Widget.Input
6567 Reflex.Dom.Widget.Lazy
6668 Reflex.Dom.Widget.Resize
6769 Reflex.Dom.Xhr
68− Reflex.Dom.Time
6970 other-modules:
7071 Reflex.Dom.Internal.Foreign
72+ Reflex.Dom.WebSocket.Foreign
7173 Reflex.Dom.Xhr.Foreign
7274
7375 other-extensions: TemplateHaskell
src-ghc/Reflex/Dom/Internal/Foreign.hs+32
@@ -1,12 +1,18 @@
11{-# LANGUAGE ForeignFunctionInterface #-}
22module Reflex.Dom.Internal.Foreign where
33
4+import Control.Lens hiding (set)
45import GHCJS.DOM hiding (runWebGUI)
56import Control.Concurrent
67import Control.Monad.State.Strict hiding (mapM, mapM_, forM, forM_, sequence, sequence_, get)
8+import Foreign.Ptr
79import GHCJS.DOM.Navigator
810import GHCJS.DOM.DOMWindow
911import Graphics.UI.Gtk hiding (Widget)
12+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSBase
13+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSObjectRef
14+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSStringRef
15+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSValueRef
1016import Graphics.UI.Gtk.WebKit.WebView
1117import Graphics.UI.Gtk.WebKit.Types hiding (Event, Widget, unWidget)
1218import Graphics.UI.Gtk.WebKit.WebSettings
@@ -14,6 +20,7 @@ import Graphics.UI.Gtk.WebKit.WebFrame
1420import Graphics.UI.Gtk.WebKit.WebInspector
1521import Data.List
1622import System.Directory
23+import System.Glib.FFI hiding (void)
1724
1825makeDefaultWebView :: String -> (WebView -> IO ()) -> IO ()
1926makeDefaultWebView userAgentKey main = do
@@ -67,3 +74,28 @@ runWebGUI' userAgentKey main = do
6774 unless ((" " ++ userAgentKey) `isSuffixOf` agent) $ main (castToWebView window)
6875 Nothing -> do
6976 makeDefaultWebView userAgentKey main
77+
78+foreign import ccall "wrapper"
79+ wrapper :: JSObjectCallAsFunctionCallback' -> IO JSObjectCallAsFunctionCallback
80+
81+toJSObject :: JSContextRef -> [Ptr OpaqueJSValue] -> IO JSObjectRef
82+toJSObject ctx args = do
83+ o <- jsobjectmake ctx nullPtr nullPtr
84+ iforM_ args $ \n a -> do
85+ prop <- jsstringcreatewithutf8cstring $ show n
86+ jsobjectsetproperty ctx o prop a 1 nullPtr
87+ return o
88+
89+fromJSStringMaybe :: JSContextRef -> JSValueRef -> IO (Maybe String)
90+fromJSStringMaybe c t = do
91+ isNull <- jsvalueisnull c t
92+ case isNull of
93+ True -> return Nothing
94+ False -> do
95+ j <- jsvaluetostringcopy c t nullPtr
96+ l <- jsstringgetmaximumutf8cstringsize j
97+ s <- allocaBytes (fromIntegral l) $ \ps -> do
98+ _ <- jsstringgetutf8cstring'_ j ps (fromIntegral l)
99+ peekCString ps
100+ return $ Just s
101+
src-ghc/Reflex/Dom/WebSocket/Foreign.hsnew file+84
@@ -0,0 +1,84 @@
1+{-# LANGUAGE ForeignFunctionInterface, JavaScriptFFI, CPP, TemplateHaskell, NoMonomorphismRestriction, EmptyDataDecls, RankNTypes, GADTs, RecursiveDo, ScopedTypeVariables, FlexibleInstances, MultiParamTypeClasses, TypeFamilies, FlexibleContexts, DeriveDataTypeable, GeneralizedNewtypeDeriving, StandaloneDeriving, ConstraintKinds, UndecidableInstances, PolyKinds, AllowAmbiguousTypes #-}
2+
3+module Reflex.Dom.WebSocket.Foreign where
4+
5+import Prelude hiding (div, span, mapM, mapM_, concat, concatMap, all, sequence)
6+
7+import Control.Exception
8+import Control.Monad.State
9+import Data.ByteString (ByteString)
10+import Data.Text.Encoding
11+import Foreign.Marshal hiding (void)
12+import Foreign.Ptr
13+import Foreign.Storable
14+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSBase
15+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSObjectRef
16+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSStringRef
17+import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSValueRef
18+import Graphics.UI.Gtk.WebKit.JavaScriptCore.WebFrame
19+import Graphics.UI.Gtk.WebKit.WebView
20+import qualified Data.ByteString as BS
21+import qualified Data.Text as T
22+
23+import Reflex.Dom.Internal.Foreign
24+
25+data JSWebSocket = JSWebSocket { wsValue :: JSValueRef
26+ , wsContext :: JSContextRef
27+ }
28+
29+getLocationHost :: WebView -> IO String
30+getLocationHost wv = do
31+ c <- webFrameGetGlobalContext =<< webViewGetMainFrame wv
32+ script <- jsstringcreatewithutf8cstring "location.host"
33+ lh <- jsevaluatescript c script nullPtr nullPtr 1 nullPtr
34+ lh' <- fromJSStringMaybe c lh
35+ return $ maybe "" id lh'
36+
37+getLocationProtocol :: WebView -> IO String
38+getLocationProtocol wv = do
39+ c <- webFrameGetGlobalContext =<< webViewGetMainFrame wv
40+ script <- jsstringcreatewithutf8cstring "location.protocol"
41+ lp <- jsevaluatescript c script nullPtr nullPtr 1 nullPtr
42+ lp' <- fromJSStringMaybe c lp
43+ return $ maybe "" id lp'
44+
45+newWebSocket :: WebView -> String -> (ByteString -> IO ()) -> IO () -> IO JSWebSocket
46+newWebSocket wv url onMessage onClose = do
47+ c <- webFrameGetGlobalContext =<< webViewGetMainFrame wv
48+ url' <- jsvaluemakestring c =<< jsstringcreatewithutf8cstring url
49+ newWSArgs <- toJSObject c [url']
50+ newWS <- jsstringcreatewithutf8cstring "(function(that) { var ws = new WebSocket(that[0]); ws['binaryType'] = 'arraybuffer'; return ws; })(this)"
51+ ws <- jsevaluatescript c newWS newWSArgs nullPtr 1 nullPtr
52+ onMessage' <- wrapper $ \_ _ _ _ args _ -> do
53+ e <- peekElemOff args 0
54+ dataProp <- jsstringcreatewithutf8cstring "data"
55+ msg <- jsobjectgetproperty c e dataProp nullPtr
56+ msg' <- fromJSStringMaybe c msg
57+ case msg' of
58+ Nothing -> return ()
59+ Just m -> onMessage $ encodeUtf8 $ T.pack m
60+ jsvaluemakeundefined c
61+ onMessageCb <- jsobjectmakefunctionwithcallback c nullPtr onMessage'
62+ onClose' <- wrapper $ \_ _ _ _ _ _ -> do
63+ onClose
64+ jsvaluemakeundefined c
65+ onCloseCb <- jsobjectmakefunctionwithcallback c nullPtr onClose'
66+ o <- toJSObject c [ws, onMessageCb, onCloseCb]
67+ addCbs <- jsstringcreatewithutf8cstring "this[0]['onmessage'] = this[1]; this[0]['onclose'] = this[2];"
68+ _ <- jsevaluatescript c addCbs o nullPtr 1 nullPtr
69+ return $ JSWebSocket ws c
70+
71+webSocketSend :: JSWebSocket -> ByteString -> IO ()
72+webSocketSend (JSWebSocket ws c) bs = do
73+ elems <- forM (BS.unpack bs) $ \x -> jsvaluemakenumber c $ fromIntegral x
74+ let numElems = length elems
75+ bs' <- bracket (mallocArray numElems) free $ \elemsArr -> do
76+ pokeArray elemsArr elems
77+ a <- jsobjectmakearray c (fromIntegral numElems) elemsArr nullPtr
78+ newUint8Array <- jsstringcreatewithutf8cstring "new Uint8Array(this)"
79+ jsevaluatescript c newUint8Array a nullPtr 1 nullPtr
80+ send <- jsstringcreatewithutf8cstring "this[0]['send'](String['fromCharCode']['apply'](null, this[1]))"
81+ sendArgs <- toJSObject c [ws, bs']
82+ _ <- jsevaluatescript c send sendArgs nullPtr 1 nullPtr
83+ return ()
84+
src-ghc/Reflex/Dom/Xhr/Foreign.hs+2−11
@@ -12,20 +12,14 @@ import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSStringRef
1212import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSValueRef
1313import Graphics.UI.Gtk.WebKit.JavaScriptCore.WebFrame
1414
15+import Reflex.Dom.Internal.Foreign
16+
1517data XMLHttpRequest
1618 = XMLHttpRequest { xhrValue :: JSValueRef
1719 , xhrContext :: JSContextRef
1820 }
1921 deriving (Eq, Ord)
2022
21−toJSObject :: JSContextRef -> [Ptr OpaqueJSValue] -> IO JSObjectRef
22−toJSObject ctx args = do
23− o <- jsobjectmake ctx nullPtr nullPtr
24− iforM_ args $ \n a -> do
25− prop <- jsstringcreatewithutf8cstring $ show n
26− jsobjectsetproperty ctx o prop a 1 nullPtr
27− return o
28−
2923responseTextToText :: Maybe String -> Maybe Text
3024responseTextToText = fmap T.pack
3125
@@ -60,9 +54,6 @@ xmlHttpRequestOpen xhr method url async user password = do
6054 _ <- jsevaluatescript c script o nullPtr 1 nullPtr
6155 return ()
6256
63−foreign import ccall "wrapper"
64− wrapper :: JSObjectCallAsFunctionCallback' -> IO JSObjectCallAsFunctionCallback
65−
6657xmlHttpRequestOnreadystatechange :: XMLHttpRequest -> IO () -> IO ()
6758xmlHttpRequestOnreadystatechange xhr userCallback = do
6859 let c = xhrContext xhr
src-ghcjs/Reflex/Dom/WebSocket/Foreign.hsnew file+49
@@ -0,0 +1,49 @@
1+{-# LANGUAGE ForeignFunctionInterface, JavaScriptFFI, CPP, TemplateHaskell, NoMonomorphismRestriction, EmptyDataDecls, RankNTypes, GADTs, RecursiveDo, ScopedTypeVariables, FlexibleInstances, MultiParamTypeClasses, TypeFamilies, FlexibleContexts, DeriveDataTypeable, GeneralizedNewtypeDeriving, StandaloneDeriving, ConstraintKinds, UndecidableInstances, PolyKinds, AllowAmbiguousTypes #-}
2+
3+module Reflex.Dom.WebSocket.Foreign where
4+
5+import Prelude hiding (div, span, mapM, mapM_, concat, concatMap, all, sequence)
6+
7+import Control.Monad hiding (forM, forM_, mapM, mapM_, sequence)
8+import Data.ByteString (ByteString)
9+import Data.Text.Encoding
10+import qualified Data.ByteString as BS
11+import Foreign.Ptr
12+import GHCJS.Foreign
13+import GHCJS.Types
14+
15+#define JS(name, js, type) foreign import javascript unsafe js name :: type
16+
17+newtype JSWebSocket = JSWebSocket { unWebSocket :: JSRef JSWebSocket }
18+
19+data JSByteArray
20+JS(extractByteArray, "new Uint8Array($1_1.buf, $1_2, $2)", Ptr a -> Int -> IO (JSRef JSByteArray))
21+
22+JS(newWebSocket_, "(function() { var ws = new WebSocket($1); ws['binaryType'] = 'arraybuffer'; ws['onmessage'] = function(e){ $2(e['data']); }; ws['onclose'] = function(e){ $3(); }; return ws; })()", JSString -> JSFun (JSString -> IO ()) -> JSFun (IO ()) -> IO (JSRef JSWebSocket))
23+
24+JS(webSocketSend_, "$1['send'](String.fromCharCode.apply(null, $2))", JSRef JSWebSocket -> JSRef JSByteArray -> IO ())
25+
26+webSocketSend :: JSWebSocket -> ByteString -> IO ()
27+webSocketSend ws bs = BS.useAsCString bs $ \cStr -> do
28+ ba <- extractByteArray cStr $ BS.length bs
29+ webSocketSend_ (unWebSocket ws) ba
30+
31+JS(getLocationHost_, "location.host", IO JSString)
32+
33+getLocationHost :: FromJSString r => a -> IO r
34+getLocationHost _ = liftM fromJSString getLocationHost_
35+
36+JS(getLocationProtocol_, "location.protocol", IO JSString)
37+
38+getLocationProtocol :: FromJSString r => a -> IO r
39+getLocationProtocol _ = liftM fromJSString getLocationProtocol_
40+
41+newWebSocket :: a -> String -> (ByteString -> IO ()) -> IO () -> IO JSWebSocket
42+newWebSocket _ url onMessage onClose = do
43+ onMessageFun <- syncCallback1 AlwaysRetain True $ onMessage <=< return . encodeUtf8 . fromJSString
44+ rec onCloseFun <- syncCallback AlwaysRetain True $ do
45+ release onMessageFun
46+ release onCloseFun
47+ onClose
48+ liftM JSWebSocket $ newWebSocket_ (toJSString url) onMessageFun onCloseFun
49+
src/Reflex/Dom.hs+2
@@ -4,6 +4,7 @@ module Reflex.Dom ( module Reflex
44 , module Reflex.Dom.Widget
55 , module Reflex.Dom.Xhr
66 , module Reflex.Dom.Time
7+ , module Reflex.Dom.WebSocket
78 ) where
89
910import Reflex
@@ -12,3 +13,4 @@ import Reflex.Dom.Internal
1213import Reflex.Dom.Widget
1314import Reflex.Dom.Xhr
1415import Reflex.Dom.Time
16+import Reflex.Dom.WebSocket
src/Reflex/Dom/WebSocket.hsnew file+63
@@ -0,0 +1,63 @@
1+{-# LANGUAGE ForeignFunctionInterface, JavaScriptFFI, CPP, TemplateHaskell, NoMonomorphismRestriction, EmptyDataDecls, RankNTypes, GADTs, RecursiveDo, ScopedTypeVariables, FlexibleInstances, MultiParamTypeClasses, TypeFamilies, FlexibleContexts, DeriveDataTypeable, GeneralizedNewtypeDeriving, StandaloneDeriving, ConstraintKinds, UndecidableInstances, PolyKinds, AllowAmbiguousTypes #-}
2+
3+module Reflex.Dom.WebSocket where
4+
5+import Prelude hiding (div, span, mapM, mapM_, concat, concatMap, all, sequence)
6+
7+import Reflex
8+import Reflex.Host.Class
9+import Reflex.Dom.Class
10+import Reflex.Dom.WebSocket.Foreign
11+
12+import Control.Concurrent
13+import Control.Lens
14+import Control.Monad hiding (forM, forM_, mapM, mapM_, sequence)
15+import Control.Monad.IO.Class
16+import Control.Monad.Ref
17+import Control.Monad.State
18+import Data.ByteString (ByteString)
19+import Data.Default
20+import Data.Dependent.Map (DSum (..))
21+import Data.IORef
22+
23+data WebSocketConfig t
24+ = WebSocketConfig { _webSocketConfig_send :: Event t [ByteString]
25+ }
26+
27+instance Reflex t => Default (WebSocketConfig t) where
28+ def = WebSocketConfig never
29+
30+data WebSocket t
31+ = WebSocket { _webSocket_recv :: Event t ByteString
32+ }
33+
34+webSocket :: forall t m. (HasWebView m, MonadWidget t m) => String -> WebSocketConfig t -> m (WebSocket t)
35+webSocket url config = do
36+ wv <- askWebView
37+ postGui <- askPostGui
38+ runWithActions <- askRunWithActions
39+ (eRecv, eRecvTriggerRef) <- newEventWithTriggerRef
40+ currentSocketRef <- liftIO $ newIORef Nothing
41+ --TODO: Disconnect if value no longer needed
42+ let onMessage :: ByteString -> IO ()
43+ onMessage m = postGui $ do
44+ mt <- readRef eRecvTriggerRef
45+ forM_ mt $ \t -> runWithActions [t :=> m]
46+ start = do
47+ ws <- liftIO $ newWebSocket wv url onMessage $ do
48+ void $ forkIO $ do --TODO: Is the fork necessary, or do event handlers run in their own threads automatically?
49+ liftIO $ writeIORef currentSocketRef Nothing
50+ liftIO $ threadDelay 1000000
51+ start
52+ liftIO $ writeIORef currentSocketRef $ Just ws
53+ return ()
54+ schedulePostBuild $ liftIO start
55+ performEvent_ $ ffor (_webSocketConfig_send config) $ \payloads -> forM_ payloads $ \payload -> do
56+ mws <- liftIO $ readIORef currentSocketRef
57+ case mws of
58+ Nothing -> return () -- Discard --TODO: should we do something better here? probably buffer it, since we handle reconnection logic; how do we verify that the server has received things?
59+ Just ws -> do
60+ liftIO $ webSocketSend ws payload
61+ return $ WebSocket eRecv
62+
63+makeLensesWith (lensRules & simpleLenses .~ True) ''WebSocketConfig