[server] Reimplement collaborative editing

This commit is contained in:
Joscha 2020-03-15 22:23:26 +00:00
parent a2d392bc4d
commit 83406dff10
6 changed files with 75 additions and 15 deletions

View file

@ -2,11 +2,16 @@
module Main where module Main where
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Monad
import qualified Network.WebSockets as WS import qualified Network.WebSockets as WS
import Forest.Node import Forest.Node
import Forest.Server.TreeApp import Forest.Server.TreeApp
{- Websocket server stuff -}
pingDelay :: Int pingDelay :: Int
pingDelay = 10 pingDelay = 10
@ -18,14 +23,68 @@ options = WS.defaultServerOptions
{ WS.serverRequirePong = Just pongDelay { WS.serverRequirePong = Just pongDelay
} }
app :: TreeApp Node () {- The actual app -}
app = TreeApp
{ appGraft = id data AppEvent = SharedNodeEdited
, appHandleEvent = \s _ -> pure $ continue s deriving (Show, Eq)
, appConstructor = simpleConstructor $ txtNode "" "Hello world"
data AppState = AppState
{ asBroadcastChan :: TChan AppEvent
, asReceiveChan :: TChan AppEvent
, asSharedNodeVar :: MVar Node
, asSharedNode :: Node
} }
graft :: AppState -> Node
graft = asSharedNode
updateSharedNode :: AppState -> (Node -> Node) -> IO AppState
updateSharedNode s f = do
node <- takeMVar $ asSharedNodeVar s
let node' = f node
putMVar (asSharedNodeVar s) node'
when (node /= node') $ atomically $ do
writeTChan (asBroadcastChan s) SharedNodeEdited
void $ readTChan $ asReceiveChan s
pure s{asSharedNode = node'}
handleEvent :: AppState -> Event AppEvent -> IO (Next AppState)
handleEvent s (Custom SharedNodeEdited) = do
node <- readMVar $ asSharedNodeVar s
pure $ continue s{asSharedNode = node}
handleEvent s (Edit path text) = do
s' <- updateSharedNode s $ adjustAt (\n -> n{nodeText = text}) path
pure $ continue s'
handleEvent s (Delete path) = do
s' <- updateSharedNode s $ deleteAt path
pure $ continue s'
handleEvent s (Reply path text) = do
s' <- updateSharedNode s $ appendAt (txtNode "edr" text) path
pure $ continue s'
handleEvent s _ = do
pure $ continue s
constructor
:: TChan AppEvent
-> MVar Node
-> (AppState -> Maybe (TChan AppEvent) -> IO a)
-> IO a
constructor broadcastChan sharedNodeVar cont = do
node <- readMVar sharedNodeVar
receiveChan <- atomically $ dupTChan broadcastChan
let state = AppState broadcastChan receiveChan sharedNodeVar node
cont state $ Just receiveChan
main :: IO () main :: IO ()
main = do main = do
putStrLn "Preparing shared editing"
sharedNodeVar <- newMVar $ txtNode "r" "Sandbox"
broadcastChan <- atomically newBroadcastTChan
let app = TreeApp
{ appGraft = graft
, appHandleEvent = handleEvent
, appConstructor = constructor broadcastChan sharedNodeVar
}
putStrLn "Starting server" putStrLn "Starting server"
WS.runServerWithOptions options $ runTreeApp pingDelay Nothing app WS.runServerWithOptions options $ runTreeApp pingDelay app

View file

@ -33,6 +33,7 @@ library
base >=4.7 && <5 base >=4.7 && <5
, forest-common , forest-common
, forest-server , forest-server
, stm
, websockets , websockets
default-language: Haskell2010 default-language: Haskell2010
@ -48,5 +49,6 @@ executable forest-cabin
, forest-cabin , forest-cabin
, forest-common , forest-common
, forest-server , forest-server
, stm
, websockets , websockets
default-language: Haskell2010 default-language: Haskell2010

View file

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

View file

@ -82,7 +82,7 @@ data Node = Node
{ nodeText :: !T.Text { nodeText :: !T.Text
, nodeFlags :: !NodeFlags , nodeFlags :: !NodeFlags
, nodeChildren :: !(OMap.OrderedMap NodeId Node) , nodeChildren :: !(OMap.OrderedMap NodeId Node)
} deriving (Show) } deriving (Show, Eq)
instance ToJSON Node where instance ToJSON Node where
toJSON node = object toJSON node = object

View file

@ -65,7 +65,7 @@ import qualified Data.Set as Set
data OrderedMap k a = OrderedMap data OrderedMap k a = OrderedMap
{ omMap :: Map.Map k a { omMap :: Map.Map k a
, omOrder :: [k] , omOrder :: [k]
} } deriving (Eq)
instance (Ord k, Show k, Show a) => Show (OrderedMap k a) where instance (Ord k, Show k, Show a) => Show (OrderedMap k a) where
show m = "fromList " ++ show (toList m) show m = "fromList " ++ show (toList m)

View file

@ -1,7 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
-- | This module specifies a structure for forest server applications. It is -- | This module specifies a structure for forest server applications. It is
-- based on the way Brick models applications. -- based on the way Brick models applications.
@ -43,7 +41,7 @@ data Event e
data TreeApp s e = TreeApp data TreeApp s e = TreeApp
{ appGraft :: s -> Node { appGraft :: s -> Node
, appHandleEvent :: s -> Event e -> IO (Next s) , appHandleEvent :: s -> Event e -> IO (Next s)
, appConstructor :: forall a. (s -> IO a) -> IO a , appConstructor :: forall a. (s -> Maybe (TChan e) -> IO a) -> IO a
} }
simpleConstructor :: s -> (s -> IO a) -> IO a simpleConstructor :: s -> (s -> IO a) -> IO a
@ -99,12 +97,12 @@ runUntilHalt conn app rs = do
sendNodeUpdate conn (rsNode rs) node' sendNodeUpdate conn (rsNode rs) node'
runUntilHalt conn app rs{rsState = state', rsNode = node'} runUntilHalt conn app rs{rsState = state', rsNode = node'}
runTreeApp :: Int -> Maybe (TChan e) -> TreeApp s e -> WS.ServerApp runTreeApp :: Int -> TreeApp s e -> WS.ServerApp
runTreeApp pingDelay customChan app pendingConn = do runTreeApp pingDelay app pendingConn = do
conn <- WS.acceptRequest pendingConn conn <- WS.acceptRequest pendingConn
chan <- atomically newTChan chan <- atomically newTChan
WS.withPingThread conn pingDelay (pure ()) $ WS.withPingThread conn pingDelay (pure ()) $
appConstructor app $ \initialState -> do appConstructor app $ \initialState customChan -> do
firstPacket <- receivePacket conn firstPacket <- receivePacket conn
case firstPacket of case firstPacket of
ClientHello _ -> do ClientHello _ -> do