[server] Extract shared editing logic into a branch
This commit is contained in:
parent
aa074d181b
commit
63a36d8a71
7 changed files with 161 additions and 43 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
80
forest-server/src/Forest/Server/Branch/SharedEdit.hs
Normal file
80
forest-server/src/Forest/Server/Branch/SharedEdit.hs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue