{-# LANGUAGE OverloadedStrings,KindSignatures, GADTs, ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
module Network.JavaScript.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(..))
start :: (Engine -> IO ())
-> Application -> Application
start kE = WS.websocketsOr WS.defaultConnectionOptions $ \ pc -> do
conn <- WS.acceptRequest pc
WS.forkPingThread conn 10
WS.sendTextData conn bootstrap
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
}
data Engine = Engine
{ sendJavaScript :: JavaScript -> IO ()
, genNonce :: IO Int
, replyBox :: Int -> IO (Either Value [Value])
, 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 = [];"
]
addListener :: Engine -> (Value -> IO ()) -> IO ThreadId
addListener e k = forkIO $ forever $ listen e >>= k
listen :: Engine -> IO Value
listen e = atomically $ fst <$> readEventChan e
readEventChan :: Engine -> STM (Value, UTCTime)
readEventChan = eventChan
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"