[server] Add new structure for server applications

This commit is contained in:
Joscha 2020-03-15 00:59:18 +00:00
parent 04b8bd7445
commit cdfe515df6
4 changed files with 128 additions and 16 deletions

View file

@ -2,15 +2,10 @@
module Main where
import Control.Concurrent.MVar
import qualified Network.WebSockets as WS
import Forest.Node
import Forest.Server
import Forest.Server.Broadcast
import Forest.Server.TreeModule.Const
import Forest.Server.TreeModule.Fork
import Forest.Server.TreeModule.SharedEditing
import Forest.Server.TreeApp
pingDelay :: Int
pingDelay = 10
@ -23,15 +18,14 @@ options = WS.defaultServerOptions
{ WS.serverRequirePong = Just pongDelay
}
app :: TreeApp Node ()
app = TreeApp
{ appGraft = id
, appHandleEvent = \s _ -> pure $ continue s
, appConstructor = simpleConstructor $ txtNode "" "Hello world"
}
main :: IO ()
main = do
putStrLn "Preparing shared edit module"
sharedEditNodeVar <- newMVar $ txtNode "r" ""
sharedEditBroadcaster <- newBroadcaster
putStrLn "Starting server"
WS.runServerWithOptions options $ serverApp pingDelay $ forkModule "Forest"
[ ProngConstructor "Test" $ constModule $ newNode "" "" [txtNode "" "Bla"]
, ProngConstructor "Sandbox" $ sharedEditingModule sharedEditNodeVar sharedEditBroadcaster
, ProngConstructor "About" $ constModule projectDescriptionNode
]
WS.runServerWithOptions options $ runTreeApp pingDelay Nothing app

View file

@ -28,6 +28,7 @@ library
exposed-modules:
Forest.Server
Forest.Server.Broadcast
Forest.Server.TreeApp
Forest.Server.TreeModule
Forest.Server.TreeModule.Animate
Forest.Server.TreeModule.Const
@ -41,6 +42,7 @@ library
base >=4.7 && <5
, containers
, forest-common
, stm
, text
, transformers
, websockets

View file

@ -15,6 +15,7 @@ dependencies:
- base >= 4.7 && < 5
- containers
- forest-common
- stm
- text
- transformers
- websockets

View file

@ -0,0 +1,115 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
-- | This module specifies a structure for forest server applications. It is
-- based on the way Brick models applications.
module Forest.Server.TreeApp
( Next
, continue
, halt
, Event(..)
, TreeApp(..)
, simpleConstructor
, runTreeApp
) where
import Control.Concurrent.STM
import Control.Monad
import Data.Function
import qualified Data.Text as T
import qualified Network.WebSockets as WS
import Forest.Api
import Forest.Node
import Forest.Util
data Next a = Continue a | Halt
continue :: a -> Next a
continue = Continue
halt :: Next a
halt = Halt
data Event e
= Edit Path T.Text
| Delete Path
| Reply Path T.Text
| Act Path
| Custom e
data TreeApp s e = TreeApp
{ appGraft :: s -> Node
, appHandleEvent :: s -> Event e -> IO (Next s)
, appConstructor :: forall a. (s -> IO a) -> IO a
}
simpleConstructor :: s -> (s -> IO a) -> IO a
simpleConstructor = (&)
{- The websocket app receiving and sending the packets -}
packetToEvent :: ClientPacket -> Maybe (Event e)
packetToEvent (ClientEdit path text) = Just $ Edit path text
packetToEvent (ClientDelete path) = Just $ Delete path
packetToEvent (ClientReply path text) = Just $ Reply path text
packetToEvent (ClientAct path) = Just $ Act path
packetToEvent (ClientHello _) = Nothing
receiveThread :: WS.Connection -> TChan (Event e) -> IO ()
receiveThread conn chan = forever $ do
packet <- receivePacket conn
case packetToEvent packet of
-- We can wrap a 'forever' around all of this because closeWithErrorMessage
-- throws a runtime exception once the connection is closed.
Nothing -> closeWithErrorMessage conn "Invalid packet: Hello"
Just event -> atomically $ writeTChan chan event
data RunState s e = RunState
{ rsEventChan :: TChan (Event e)
, rsCustomEventChan :: Maybe (TChan e)
, rsState :: s
, rsNode :: Node
}
readEvent :: RunState s e -> STM (Event e)
readEvent rs = case rsCustomEventChan rs of
Nothing -> readTChan ec
Just cec -> readTChan ec `orElse` (Custom <$> readTChan cec)
where
ec = rsEventChan rs
sendNodeUpdate :: WS.Connection -> Node -> Node -> IO ()
sendNodeUpdate conn nodeOld nodeNew = case diffNodes nodeOld nodeNew of
Nothing -> putStrLn "Sending no update because the node didn't change"
Just (path, updatedNode) -> do
putStrLn $ "Sending partial update at " ++ show path ++ ": " ++ show updatedNode
sendPacket conn $ ServerUpdate path updatedNode
runUntilHalt :: WS.Connection -> TreeApp s e -> RunState s e -> IO ()
runUntilHalt conn app rs = do
event <- atomically $ readEvent rs
next <- appHandleEvent app (rsState rs) event
case next of
Halt -> pure ()
Continue state' -> do
let node' = appGraft app state'
sendNodeUpdate conn (rsNode rs) node'
runUntilHalt conn app rs{rsState = state', rsNode = node'}
runTreeApp :: Int -> Maybe (TChan e) -> TreeApp s e -> WS.ServerApp
runTreeApp pingDelay customChan app pendingConn = do
conn <- WS.acceptRequest pendingConn
chan <- atomically newTChan
WS.withPingThread conn pingDelay (pure ()) $
appConstructor app $ \initialState -> do
firstPacket <- receivePacket conn
case firstPacket of
ClientHello _ -> do
let initialNode = appGraft app initialState
rs = RunState chan customChan initialState initialNode
sendPacket conn $ ServerHello [] initialNode
withThread (receiveThread conn chan) $ runUntilHalt conn app rs
_ -> closeWithErrorMessage conn "Invalid packet: Expected hello"