packages feed

haste-app-0.1.0.0: src/Haste/App/Server.hs

{-# LANGUAGE CPP, OverloadedStrings #-}
module Haste.App.Server (serverLoop) where
import Haste.App.Transport
import Haste.App.Server.Type
import Data.Typeable

#ifdef __HASTE__
serverLoop _ _ = pure undefined
instance Typeable a => MonadClient (EnvServer a) where
  remoteCall _ _ _ = pure undefined
#else
import Control.Concurrent
import Control.Monad.Catch
import Control.Monad ((>=>))
import qualified Data.ByteString.Lazy as BSL
import Data.ByteString.Lazy.UTF8
import GHC.StaticPtr
import Unsafe.Coerce
import Network.WebSockets as WS
import Haste.Serialize
import Haste.JSON
import Haste (JSString, fromJSStr, toJSString)
import Haste.App.Protocol
import Haste.App.Routing (NodeEnv (..))
import Haste.Concurrent (CIO, concurrent, liftIO)

import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp as W
import Network.Wai.Handler.Warp.Internal as W (settingsPort)
import Network.Wai.Handler.WebSockets

import Control.Exception (SomeException (..))
import qualified Control.Exception as CE (catch, throwIO)

instance MonadThrow CIO where
  throwM = unsafeCoerce . CE.throwIO
instance MonadCatch CIO where
  catch m h = unsafeCoerce $ CE.catch (unsafeCoerce m) (unsafeCoerce . h)

-- | Run the server event loop for a single endpoint.
serverLoop :: NodeEnv m -> Int -> IO ()
serverLoop env port = do
    run port $ websocketsOr defaultConnectionOptions handleWS handleHttp
  where
    handleWS = acceptRequest >=> clientLoop

    clientLoop c = do
      msg <- toJSString . toString <$> receiveData c
      _ <- forkIO $ handlePacket env c msg
      clientLoop c

    handleHttp _ resp = do
      resp $ responseLBS status404 [] "WebSockets only"

handlePacket :: NodeEnv m -> Connection -> JSString -> IO ()
handlePacket env c msg = do
  case decodeJSON msg >>= fromJSON of
    Right (ServerCall nonce method args) -> handleCall env c nonce method args
    _                                    -> error "invalid server call"

-- | Handle a call to this specific node. Note that the method itself is
--   executed in the CIO monad by the handler.
handleCall :: NodeEnv m -> Connection -> Nonce -> StaticKey -> [JSON] -> IO ()
handleCall (NodeEnv env) c nonce method args = concurrent $ do
  mm <- liftIO $ unsafeLookupStaticPtr method
  case mm of
    Just m -> do
      result <- try $ deRefStaticPtr m env args
      let reply = case result of
            Right r                -> ServerReply nonce r
            Left (SomeException e) -> ServerEx nonce (show e)
      liftIO $ sendTextData c $ fromString $ fromJSStr $ encodeJSON $ toJSON reply
    _ -> do
      error $ "no such method: " ++ show method

instance Typeable a => MonadClient (EnvServer a) where
  remoteCall (WebSocket h p) msg n = liftIO $ do
    WS.runClient h p "/" $ \ c' -> do
      sendTextData c' (fromString $ fromJSStr msg)
      reply <- toJSString . toString <$> receiveData c'
      case decodeJSON reply >>= fromJSON of
        Right (ServerReply n' msg) -> return msg
        Right (ServerEx _ msg)     -> throwM (ServerException msg)
        Left e                     -> throwM (NetworkException $ show e)
#endif