packages feed

purview-0.2.0.0: src/EventLoop.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}

module EventLoop
 ( eventLoop )
where

import           Control.Concurrent.STM.TChan
import           Control.Monad.STM
import           Control.Monad
import           Control.Concurrent
import           Data.Typeable
import           Data.Aeson (encode)
import qualified Network.WebSockets as WebSockets

import           Component
import           Diffing
import           EventHandling
import           Events
import           PrepareTree
import           Rendering
import           CleanTree (cleanTree)
import           CollectInitials (collectInitials)

type Log m = String -> m ()

eventLoop'
  :: (Monad m, Typeable event)
  => Event
  -> Bool
  -> (m [Event] -> IO [Event])
  -> Log IO
  -> TChan Event
  -> WebSockets.Connection
  -> Purview event m
  -> IO (Maybe (Purview event m))
eventLoop' message devMode runner log eventBus connection component = do
  -- if it's special newState event, the state is replaced in the tree
  let newTree = case message of
        FromFrontendEvent {}                 -> component
        InternalEvent {}                     -> component
        JavascriptCallEvent {}               -> component
        stateChangeEvent@StateChangeEvent {} -> applyNewState stateChangeEvent component

  let
    -- 1. adds locations
    locatedTree = prepareTree newTree
    -- 2. collects css and initial events
    (initialEvents, css) = collectInitials locatedTree
    -- 3. removes captured css and initial events
    newTree' = cleanTree css locatedTree
    -- why pass in the found css?  otherwise haskell will optimize by
    -- putting the function that removes css into the tree, which results
    -- in the css being removed before it's been found by collectInitials.
    -- or at least, that's what seemed to be happening.  very funny.

    event = findEvent message newTree'

  -- this is where handlers are actually called, and their events are sent back into
  -- this loop
  void . forkIO $ do
    newEvents <- case (event, message) of
      (_, event'@InternalEvent {}) -> runner $ runEvent event' newTree'
      (Just event', _)             -> runner $ runEvent event' newTree'
      (Nothing,     _)             -> pure []
    mapM_ (atomically . writeTChan eventBus) newEvents

  let
    -- collect diffs
    location = case message of
      (FromFrontendEvent { location }) -> location
      (StateChangeEvent _ location)    -> location
      (InternalEvent { handlerId })    -> handlerId
      (JavascriptCallEvent {})         -> error "tried to get location off javascript call"

    diffs = diff location [0] component newTree'
    -- for now it's just "Update", which the javascript handles as replacing
    -- the html beneath the handler.  I imagine it could be more exact, with
    -- Delete / Create events.
    renderedDiffs = fmap (\(Update location graph) -> Update location (render graph)) diffs

  unless (null css) $ WebSockets.sendTextData
    connection
    (encode $ ForFrontEndEvent { event = "setCSS", message = css })

  WebSockets.sendTextData
    connection
    (encode $ ForFrontEndEvent { event = "setHtml", message = renderedDiffs })

  pure (Just newTree')

handleJavascriptCall :: String -> String -> WebSockets.Connection -> IO (Maybe (Purview event m))
handleJavascriptCall name value connection = do
  WebSockets.sendTextData
    connection
    (encode $ ForFrontEndEvent { event = "callJS", message = [name, value] })

  pure Nothing

--
-- This is the main event loop of handling messages from the websocket
--
-- pretty much just get a message, then run the message via the component
-- handler, and then send the "setHtml" back downstream to tell it to replace
-- the html with the new.
--
eventLoop
  :: (Monad m, Typeable event)
  => Bool
  -> (m [Event] -> IO [Event])
  -> Log IO
  -> TChan Event
  -> WebSockets.Connection
  -> Purview event m
  -> IO ()
eventLoop devMode runner log eventBus connection component = do
  message <- atomically $ readTChan eventBus

  when devMode $ log $ show message

  newTree <- case message of
    JavascriptCallEvent name value -> handleJavascriptCall name value connection
    _ -> eventLoop' message devMode runner log eventBus connection component

  case newTree of
    Just tree ->
      case message of
        (FromFrontendEvent { kind }) -> do
          when (devMode && kind == "init") $
            WebSockets.sendTextData
            connection
            (encode $ ForFrontEndEvent { event = "setHtml", message = [ Update [] (render tree) ] })

          eventLoop devMode runner log eventBus connection tree
        _ ->
          eventLoop devMode runner log eventBus connection tree
    Nothing -> eventLoop devMode runner log eventBus connection component