[server] Extract shared editing logic into a branch

This commit is contained in:
Joscha 2020-03-17 20:29:18 +00:00
parent aa074d181b
commit 63a36d8a71
7 changed files with 161 additions and 43 deletions

View file

@ -1,13 +1,16 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where module Main where
import Control.Concurrent.MVar
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad import Lens.Micro
import Lens.Micro.TH
import qualified Network.WebSockets as WS import qualified Network.WebSockets as WS
import Forest.Node import Forest.Node
import Forest.Server.Branch.SharedEdit
import Forest.Server.Schema
import Forest.Server.TreeApp import Forest.Server.TreeApp
{- Websocket server stuff -} {- Websocket server stuff -}
@ -25,64 +28,51 @@ options = WS.defaultServerOptions
{- The actual app -} {- The actual app -}
data AppEvent = SharedNodeEdited data AppEvent = UpdateSharedEdit
deriving (Show, Eq) deriving (Show, Eq)
data AppState = AppState newtype AppState = AppState
{ asBroadcastChan :: TChan AppEvent { _asSharedEdit :: SharedEditLocal
, asReceiveChan :: TChan AppEvent
, asSharedNodeVar :: MVar Node
, asSharedNode :: Node
} }
draw :: AppState -> Node makeLenses ''AppState
draw = asSharedNode
updateSharedNode :: AppState -> (Node -> Node) -> IO AppState schema :: AppState -> Schema (Branch AppState AppEvent)
updateSharedNode s f = do schema s = fork' "Forest"
node <- takeMVar $ asSharedNodeVar s [ leaf $ schemaLift asSharedEdit sharedEditBranch s
let node' = f node ]
putMVar (asSharedNodeVar s) node'
when (node /= node') $ atomically $ do draw :: AppState -> Node
writeTChan (asBroadcastChan s) SharedNodeEdited draw = schemaDraw . schema
void $ readTChan $ asReceiveChan s
pure s{asSharedNode = node'}
handleEvent :: AppState -> Event AppEvent -> IO (Next AppState) handleEvent :: AppState -> Event AppEvent -> IO (Next AppState)
handleEvent s (Custom SharedNodeEdited) = do handleEvent s (Custom UpdateSharedEdit) = do
node <- readMVar $ asSharedNodeVar s sel' <- sharedEditUpdate $ s ^. asSharedEdit
pure $ continue s{asSharedNode = node} pure $ continue $ s & asSharedEdit .~ sel'
handleEvent s (Edit path text) = do handleEvent s e = case schemaHandleEvent (schema s) e of
s' <- updateSharedNode s $ adjustAt (\n -> n{nodeText = text}) path Nothing -> pure $ continue s
pure $ continue s' Just s' -> 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 _ = pure $ continue s
constructor constructor
:: TChan AppEvent :: TChan AppEvent
-> MVar Node -> SharedEditGlobal
-> (AppState -> Maybe (TChan AppEvent) -> IO a) -> (AppState -> Maybe (TChan AppEvent) -> IO a)
-> IO a -> IO a
constructor broadcastChan sharedNodeVar cont = do constructor broadcastChan seg cont = do
node <- readMVar sharedNodeVar sel <- sharedEditLocal seg
receiveChan <- atomically $ dupTChan broadcastChan receiveChan <- atomically $ dupTChan broadcastChan
let state = AppState broadcastChan receiveChan sharedNodeVar node cont (AppState sel) (Just receiveChan)
cont state $ Just receiveChan
main :: IO () main :: IO ()
main = do main = do
putStrLn "Preparing shared editing" putStrLn "Preparing server"
sharedNodeVar <- newMVar $ txtNode "r" "Sandbox"
broadcastChan <- atomically newBroadcastTChan broadcastChan <- atomically newBroadcastTChan
let onEditChange = atomically $ writeTChan broadcastChan UpdateSharedEdit
seg <- sharedEditGlobal onEditChange "Sandbox"
let app = TreeApp let app = TreeApp
{ appDraw = draw { appDraw = draw
, appHandleEvent = handleEvent , appHandleEvent = handleEvent
, appConstructor = constructor broadcastChan sharedNodeVar , appConstructor = constructor broadcastChan seg
} }
putStrLn "Starting server" putStrLn "Starting server"

View file

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

View file

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

View file

@ -26,6 +26,7 @@ source-repository head
library library
exposed-modules: exposed-modules:
Forest.Server.Branch.SharedEdit
Forest.Server.Schema Forest.Server.Schema
Forest.Server.TreeApp Forest.Server.TreeApp
other-modules: other-modules:
@ -36,6 +37,7 @@ library
base >=4.7 && <5 base >=4.7 && <5
, containers , containers
, forest-common , forest-common
, microlens
, stm , stm
, text , text
, transformers , transformers

View file

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

View file

@ -0,0 +1,80 @@
module Forest.Server.Branch.SharedEdit
( SharedEditGlobal
, sharedEditGlobal
, SharedEditLocal
, sharedEditLocal
, sharedEditDraw
, sharedEditUpdate
, sharedEditHandleEvent
, sharedEditBranch
) where
import Control.Concurrent
import Control.Monad
import qualified Data.Text as T
import Forest.Node
import Forest.Server.Schema
import Forest.Server.TreeApp
data SharedEditGlobal = SharedEditGlobal
{ seOnUpdate :: IO ()
, seNodeVar :: MVar Node
}
sharedEditGlobal :: IO () -> T.Text -> IO SharedEditGlobal
sharedEditGlobal onUpdate initialText = do
nodeVar <- newMVar $ txtNode "r" initialText
pure SharedEditGlobal
{ seOnUpdate = onUpdate
, seNodeVar = nodeVar
}
data SharedEditLocal = SharedEditLocal
{ seGlobal :: SharedEditGlobal
, seNode :: Node
}
sharedEditLocal :: SharedEditGlobal -> IO SharedEditLocal
sharedEditLocal seg = do
node <- readMVar $ seNodeVar seg
pure SharedEditLocal
{ seGlobal = seg
, seNode = node
}
sharedEditDraw :: SharedEditLocal -> Node
sharedEditDraw = seNode
sharedEditUpdate :: SharedEditLocal -> IO SharedEditLocal
sharedEditUpdate sel = do
node <- readMVar $ seNodeVar $ seGlobal sel
pure sel{seNode = node}
updateNode :: SharedEditLocal -> (Node -> Node) -> IO SharedEditLocal
updateNode sel f = do
let seg = seGlobal sel
nodeVar = seNodeVar seg
node <- takeMVar nodeVar
let node' = f node
putMVar nodeVar node'
when (node /= node') $ seOnUpdate seg
pure sel{seNode = node'}
sharedEditHandleEvent :: SharedEditLocal -> Path -> Event e -> IO SharedEditLocal
-- Ignore edits to the top node since it's only reply-able, not edit-able
sharedEditHandleEvent sel (Path []) (Edit _ _) = pure sel
sharedEditHandleEvent sel (Path []) (Delete _) = pure sel
sharedEditHandleEvent sel path (Edit _ text) =
updateNode sel $ adjustAt (\n -> n {nodeText = text}) path
sharedEditHandleEvent sel path (Delete _) =
updateNode sel $ deleteAt path
sharedEditHandleEvent sel path (Reply _ text) =
updateNode sel $ appendAt (txtNode "edr" text) path
sharedEditHandleEvent sel _ _ = pure sel
sharedEditBranch :: SharedEditLocal -> Branch SharedEditLocal e
sharedEditBranch sel = Branch
{ branchNode = sharedEditDraw sel
, branchHandleEvent = sharedEditHandleEvent sel
}

View file

@ -1,3 +1,5 @@
{-# LANGUAGE RankNTypes #-}
module Forest.Server.Schema module Forest.Server.Schema
( Schema ( Schema
, fork , fork
@ -6,12 +8,19 @@ module Forest.Server.Schema
, collect , collect
, collectWith , collectWith
, dispatch , dispatch
-- * Useful type
, Branch(..)
, schemaDraw
, schemaHandleEvent
, schemaLift
) where ) where
import qualified Data.Text as T import qualified Data.Text as T
import Lens.Micro
import Forest.Node import Forest.Node
import qualified Forest.OrderedMap as OMap import qualified Forest.OrderedMap as OMap
import Forest.Server.TreeApp
data Schema a data Schema a
= Fork T.Text (OMap.OrderedMap NodeId (Schema a)) = Fork T.Text (OMap.OrderedMap NodeId (Schema a))
@ -44,3 +53,33 @@ dispatch :: Path -> Schema a -> Maybe (Path, a)
dispatch path (Leaf a) = Just (path, a) dispatch path (Leaf a) = Just (path, a)
dispatch (Path (x:xs)) (Fork _ omap) = dispatch (Path xs) =<< (omap OMap.!? x) dispatch (Path (x:xs)) (Fork _ omap) = dispatch (Path xs) =<< (omap OMap.!? x)
dispatch (Path []) (Fork _ _ ) = Nothing -- More specfic than required dispatch (Path []) (Fork _ _ ) = Nothing -- More specfic than required
data Branch s e = Branch
{ branchNode :: Node
, branchHandleEvent :: Path -> Event e -> IO s
}
schemaDraw :: Schema (Branch s e) -> Node
schemaDraw = collectWith branchNode
schemaHandleEvent :: Schema (Branch s e) -> Event e -> Maybe (IO s)
schemaHandleEvent schema event = do
path <- getPath event
(relPath, branch) <- dispatch path schema
pure $ branchHandleEvent branch relPath event
where
getPath (Edit path _) = Just path
getPath (Delete path) = Just path
getPath (Reply path _) = Just path
getPath (Act path) = Just path
getPath _ = Nothing
schemaLift :: Lens' s t -> (t -> Branch t e) -> s -> Branch s e
schemaLift l f s = Branch
{ branchNode = branchNode branch
, branchHandleEvent = \path event -> do
t' <- branchHandleEvent branch path event
pure $ s & l .~ t'
}
where
branch = f $ s ^. l