javascript-bridge-0.2.0: src/Network/JavaScript/Services.hs
{-# LANGUAGE OverloadedStrings,KindSignatures, GADTs, ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
module Network.JavaScript.Services
( -- * Web Services
Engine(..)
, start
, addListener
, listen
, readEventChan
, Application
) where
import Control.Applicative((<|>))
import qualified Data.Text.Lazy as LT
import Data.Time.Clock
import qualified Network.Wai.Handler.WebSockets as WS
import Network.Wai (Application)
import qualified Network.WebSockets as WS
import Control.Concurrent (forkIO, ThreadId)
import Control.Exception (try, SomeException)
import Control.Monad (forever)
import Control.Concurrent.STM
import Data.Aeson (Value(..), decode', FromJSON(..),withObject,(.:))
import qualified Data.IntMap.Strict as IM
import Network.JavaScript.Internal(JavaScript(..))
-- | This accepts WebSocket requests, calls the callback with
-- an 'Engine' that can be used to access JavaScript.
start :: (Engine -> IO ())
-> Application -> Application
start kE = WS.websocketsOr WS.defaultConnectionOptions $ \ pc -> do
conn <- WS.acceptRequest pc
-- Use ping to keep connection alive
WS.forkPingThread conn 10
-- Bootstrap the remote handler
WS.sendTextData conn bootstrap
-- Handling packets
nonceRef <- newTVarIO 0
replyMap <- newTVarIO IM.empty
eventQueue <- newTChanIO
let catchMe m = try m >>= \ (_ :: Either SomeException ()) -> return ()
_ <- forkIO $ catchMe $ forever $ do
d <- WS.receiveData conn
case decode' d of
Just (Result _ []) -> return ()
Just (Result n replies) -> atomically
$ modifyTVar replyMap
$ IM.insert n
$ Right
$ replies
Just (Error n obj) -> atomically
$ modifyTVar replyMap
$ IM.insert n
$ Left
$ obj
Just (Event event) -> do
utc <- getCurrentTime
atomically $ writeTChan eventQueue (event,utc)
Nothing -> print ("bad (non JSON) reply from JavaScript"::String,d)
kE $ Engine
{ sendJavaScript = \ (JavaScript js) -> WS.sendTextData conn js
, genNonce = atomically $ do
n <- readTVar nonceRef
writeTVar nonceRef $ succ n
return n
, replyBox = \ n -> atomically $ do
t <- readTVar replyMap
case IM.lookup n t of
Nothing -> retry
Just v -> return v
, eventChan = readTChan eventQueue
}
-- | An 'Engine' is a handle to a specific JavaScript engine
data Engine = Engine
{ sendJavaScript :: JavaScript -> IO () -- send text to the JS engine
, genNonce :: IO Int -- nonce generator
, replyBox :: Int -> IO (Either Value [Value]) -- reply mailbox
, eventChan :: STM (Value, UTCTime)
}
bootstrap :: LT.Text
bootstrap = LT.unlines
[ "jsb.event = function(ev) {"
, " if (jsb.debug) { console.log('event',{event: ev}); }"
, " jsb.ws.send(JSON.stringify({event: ev}));"
, " };"
, "jsb.error = function(n,err) {"
, " if (jsb.debug) { console.log('send',{id: n, error: err}); }"
, " jsb.ws.send(JSON.stringify({id: n, error: err}));"
, " throw(err);"
, " };"
, "jsb.reply = function(n,obj) {"
, " Promise.all(obj).then(function(obj){"
, " if (jsb.debug) { console.log('reply',{id:n, result:obj}); }"
, " jsb.ws.send(JSON.stringify({id: n, result: obj}));"
, " }).catch(function(err){"
, " jsb.error(n,err);"
, " });"
, " };"
, "jsb.ws.onmessage = function(evt){ "
, " if (jsb.debug) { console.log('eval',evt.data); }"
, " eval('(function(){' + evt.data + '})()');"
, "};"
, "jsb.rs = [];"
]
--
-- | Add a listener for events. There can be many. non-blocking.
--
-- From JavaScript, you can call event(..) to send
-- values to this listener. Any valid JSON value can be sent.
addListener :: Engine -> (Value -> IO ()) -> IO ThreadId
addListener e k = forkIO $ forever $ listen e >>= k
-- | 'listen' for the next event. blocking.
--
-- From JavaScript, you can call event(..) to send
-- values to this listener. Any valid JSON value can be sent.
--
--
listen :: Engine -> IO Value
listen e = atomically $ fst <$> readEventChan e
-- | 'readEventChan' uses STM to read the next event.
--
-- From JavaScript, you can call event(..) to send
-- values to this channel. Any valid JSON value can be sent.
--
--
readEventChan :: Engine -> STM (Value, UTCTime)
readEventChan = eventChan
------------------------------------------------------------------------------
-- This is what we send back from JavaScript.
data Reply = Result Int [Value]
| Error Int Value
| Event Value
deriving Show
instance FromJSON Reply where
parseJSON o = parseEvent o
<|> parseResult o
<|> parseError o
where
parseEvent = withObject "Event" $ \v -> Event
<$> v .: "event"
parseResult = withObject "Result" $ \v -> Result
<$> v .: "id"
<*> v .: "result"
parseError = withObject "Error" $ \v -> Error
<$> v .: "id"
<*> v .: "error"