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