packages feed

purview-0.2.0.0: src/Purview/Server.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

module Purview.Server
  ( serve
  , Configuration (..)
  , defaultConfiguration
  , renderFullPage
  , startWebSocketLoop
  )
where

import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.WebSockets as WebSocket
import qualified Network.Wai.Handler.WebSockets as WaiWebSocket
import           Network.HTTP.Types ( status200 )
import qualified Data.ByteString.Char8 as ByteString
import           Data.ByteString.Builder.Internal
import qualified Data.Text as Text
import           Data.Typeable
import           Control.Concurrent.STM.TChan
import           Control.Monad.STM
import           Control.Concurrent
import           Data.Aeson
import           Blaze.ByteString.Builder.Char.Utf8

import           Component
import           EventLoop
import           Events
import           PrepareTree
import           Rendering
import           Wrapper
import           CollectInitials
import           CleanTree
import           Configuration

defaultConfiguration :: Configuration IO
defaultConfiguration = Configuration
  { interpreter       = id
  , logger            = putStrLn
  , eventsToListenTo  = [ "click", "focusout", "focusin", "change", "submit" ]
  , htmlHead          = ""
  , devMode           = False
  , javascript        = ""
  , port              = 8001
  , secure            = False
  }

{-|

This starts up the Warp server.

__Example:__

@
import Purview.Server

view url = p [ text "hello world" ]

main = serve defaultConfiguration view
@

-}
serve :: Monad m => Configuration m -> (String -> Purview () m) -> IO ()
serve config@Configuration{ port, logger } component =
  let
    settings = Warp.setPort port Warp.defaultSettings
  in do
    logger $ "Starting on port " <> show port

    Warp.runSettings settings
      $ WaiWebSocket.websocketsOr
          WebSocket.defaultConnectionOptions
          (webSocketHandler config component)
          (httpHandler config component)

webSocketHandler
  :: Monad m
  => Configuration m
  -> (String -> Purview () m)
  -> WebSocket.PendingConnection -> IO ()
webSocketHandler config component pendingConnection = do
  let
    path = ByteString.unpack
      $ WebSocket.requestPath (WebSocket.pendingRequest pendingConnection)
    render = component path

  connection <- WebSocket.acceptRequest pendingConnection
  startWebSocketLoop config { devMode=True } render connection

httpHandler :: Configuration m -> (String -> Purview () m) -> Wai.Application
httpHandler config component request respond =
  let
    path = Text.unpack . Text.concat $ Wai.pathInfo request
    render = component $ "/" <> path
  in
    respond
      $ Wai.responseBuilder
          status200
          [("Content-Type", "text/html")]
          (renderFullPage config render)

renderFullPage :: Typeable action => Configuration m -> Purview action m -> Builder
renderFullPage Configuration { htmlHead, eventsToListenTo, javascript, secure } component =
  let
    locatedComponent = prepareTree component
    (initialEvents, css) = collectInitials locatedComponent
    rendered = render (cleanTree css locatedComponent)
    wrap = wrapHtml css htmlHead eventsToListenTo javascript secure
  in
    fromString $ wrap rendered

startWebSocketLoop
  :: (Monad m, Typeable action)
  => Configuration m
  -> Purview action m
  -> WebSocket.Connection
  -> IO ()
startWebSocketLoop Configuration { devMode, interpreter, logger } component connection = do
  eventBus <- newTChanIO

  atomically
    $ writeTChan eventBus
    $ FromFrontendEvent { kind = "init", childLocation = Nothing, location = Nothing, value = Nothing }

  WebSocket.withPingThread connection 30 (pure ()) $ do
    _ <- forkIO $ webSocketMessageHandler eventBus connection
    eventLoop devMode interpreter logger eventBus connection component

webSocketMessageHandler :: TChan Event -> WebSocket.Connection -> IO ()
webSocketMessageHandler eventBus websocketConnection = do
  message' <- WebSocket.receiveData websocketConnection

  case decode message' of
    Just fromEvent -> atomically $ writeTChan eventBus fromEvent
    Nothing -> do
      print $ "error: failed to decode event: " <> message'
      print "this may be an error in Purview so feel free to open an issue"
      pure ()

  webSocketMessageHandler eventBus websocketConnection