packages feed

replica-0.1.0.0: src/Network/Wai/Handler/Replica.hs

{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Network.Wai.Handler.Replica where

import           Control.Concurrent             (Chan, forkIO, newChan, readChan, writeChan)
import           Control.Monad                  (forever, void)

import           Data.Aeson                     ((.:), (.=))
import qualified Data.Aeson                     as A

import qualified Data.ByteString                as B
import qualified Data.ByteString.Lazy           as BL
import qualified Data.Text                      as T
import           Network.HTTP.Types             (status200)

import           Network.WebSockets             (ServerApp)
import           Network.WebSockets.Connection  (ConnectionOptions, Connection, acceptRequest, forkPingThread, receiveData, sendTextData)
import           Network.Wai                    (Application, responseLBS)
import           Network.Wai.Handler.WebSockets (websocketsOr)

import qualified Replica.VDOM                   as V

import           Debug.Trace                    (traceIO)

data Event = Event
  { evtType        :: T.Text
  , evtEvent       :: A.Value
  , evtPath        :: [Int]
  , evtClientFrame :: Int
  } deriving Show

instance A.FromJSON Event where
  parseJSON (A.Object o) = Event
    <$> o .: "eventType"
    <*> o .: "event"
    <*> o .: "path"
    <*> o .: "clientFrame"
  parseJSON _ = fail "Expected object"

data Update
  = ReplaceDOM V.HTML
  | UpdateDOM Int Int [V.Diff]

instance A.ToJSON Update where
  toJSON (ReplaceDOM dom) = A.object
    [ "type" .= V.t "replace"
    , "dom"  .= dom
    ]
  toJSON (UpdateDOM serverFrame clientFrame ddiff) = A.object
    [ "type" .= V.t "update"
    , "serverFrame" .= serverFrame
    , "clientFrame" .= clientFrame
    , "diff" .= ddiff
    ]

app :: forall st.
     B.ByteString
  -> ConnectionOptions
  -> st
  -> (st -> IO (Maybe (V.HTML, Event -> IO st)))
  -> Application
app title options initial step
  = websocketsOr options (websocketApp initial step) backupApp
  where
    indexBS = BL.fromStrict $ V.index title

    backupApp :: Application
    backupApp _ respond = respond $ responseLBS status200 [] indexBS

websocketApp :: forall st.
     st
  -> (st -> IO (Maybe (V.HTML, Event -> IO st)))
  -> ServerApp
websocketApp initial step pendingConn = do
  conn <- acceptRequest pendingConn
  chan <- newChan

  forkPingThread conn 30
    
  _ <- forkIO $ forever $ do
    msg <- receiveData conn
    case A.decode msg of
      Just e  -> writeChan chan e
      Nothing -> traceIO $ "Couldn't decode event: " <> show msg

  void $ go conn chan Nothing initial 0 0

  where
    go :: Connection -> Chan Event -> Maybe V.HTML -> st -> Int -> Int -> IO ()
    go conn chan oldDom st serverFrame clientFrame = do
      r <- step st
      case r of
        Nothing -> pure ()
        Just (newDom, await) -> do
          case oldDom of
            Nothing -> sendTextData conn $ A.encode $ ReplaceDOM newDom
            Just oldDom' -> sendTextData conn $ A.encode $ UpdateDOM serverFrame clientFrame (V.diff oldDom' newDom)
          
          event <- readChan chan
          newSt <- await event
    
          go conn chan (Just newDom) newSt (serverFrame + 1) (evtClientFrame event)