{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Miso.Internal
( initialize
, componentMap
, sink
, mail
, notify
, runView
, sample
, Prerender(..)
) where
import Control.Exception (throwIO)
import Control.Concurrent (ThreadId, killThread, threadDelay)
import Control.Monad (forM, forM_, when, void)
import Control.Monad.IO.Class
import qualified Data.Aeson as A
import Data.Foldable (toList)
import Data.IORef (IORef, newIORef, atomicModifyIORef', readIORef, atomicWriteIORef, modifyIORef')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified Data.Sequence as S
import qualified JavaScript.Array as JSArray
import Language.Javascript.JSaddle
import Prelude hiding (null)
import System.IO.Unsafe (unsafePerformIO)
import System.Mem.StableName (makeStableName)
import Text.HTML.TagSoup (Tag(..))
import Text.HTML.TagSoup.Tree (parseTree, TagTree(..))
import Miso.Concurrent (Waiter(..), waiter)
import Miso.Delegate (delegator, undelegator)
import Miso.Diff (diff)
import Miso.Exception (MisoException(..), exception)
import qualified Miso.FFI as FFI
import Miso.Html hiding (on)
import Miso.String hiding (reverse)
import Miso.Types hiding (componentName)
import Miso.Event (Events)
import Miso.Effect (Sink, Effect, runEffect)
initialize
:: Eq model
=> App effect model action a
-> (Sink action -> JSM (MisoString, JSVal, IORef VTree))
-> JSM (IORef VTree)
initialize App {..} getView = do
Waiter {..} <- liftIO waiter
componentActions <- liftIO (newIORef S.empty)
let
componentSink = \action -> liftIO $ do
atomicModifyIORef' componentActions $ \actions -> (actions S.|> action, ())
serve
componentSubThreads <- forM subs $ \sub -> FFI.forkJSM (sub componentSink)
(componentName, componentMount, componentVTree) <- getView componentSink
componentModel <- liftIO (newIORef model)
let
eventLoop !oldModel = liftIO wait >> do
as <- liftIO $ atomicModifyIORef' componentActions $ \actions -> (S.empty, actions)
newModel <- foldEffects translate update componentSink (toList as) oldModel
oldName <- liftIO $ oldModel `seq` makeStableName oldModel
newName <- liftIO $ newModel `seq` makeStableName newModel
when (oldName /= newName && oldModel /= newModel) $ do
newVTree <- runView DontPrerender (view newModel) componentSink logLevel events
oldVTree <- liftIO (readIORef componentVTree)
void waitForAnimationFrame
diff componentMount (Just oldVTree) (Just newVTree)
liftIO $ do
atomicWriteIORef componentVTree newVTree
atomicWriteIORef componentModel newModel
syncPoint
eventLoop newModel
componentMainThread <- FFI.forkJSM (eventLoop model)
registerComponent ComponentState {..}
delegator componentMount componentVTree events (logLevel `elem` [DebugEvents, DebugAll])
forM_ initialAction componentSink
pure componentVTree
data Prerender
= DontPrerender
| Prerender
deriving (Show, Eq)
data ComponentState model action
= ComponentState
{ componentName :: MisoString
, componentMainThread :: ThreadId
, componentSubThreads :: [ThreadId]
, componentMount :: JSVal
, componentVTree :: IORef VTree
, componentSink :: action -> JSM ()
, componentModel :: IORef model
}
componentMap :: IORef (Map MisoString (ComponentState model action))
{-# NOINLINE componentMap #-}
componentMap = unsafePerformIO (newIORef mempty)
sample
:: Component effect model action a
-> JSM model
sample (Component _ name _) = do
componentStateMap <- liftIO (readIORef componentMap)
liftIO $ case M.lookup name componentStateMap of
Nothing -> throwIO (NotMountedException name)
Just ComponentState {..} -> readIORef componentModel
notify
:: Component effect model action a
-> action
-> JSM ()
notify (Component _ name _) action = io
where
io = do
componentStateMap <- liftIO (readIORef componentMap)
forM_ (M.lookup name componentStateMap) $ \ComponentState {..} ->
componentSink action
foldEffects
:: (effect model action a -> Effect model action a)
-> (action -> effect model action a)
-> Sink action
-> [action]
-> model
-> JSM model
foldEffects _ _ _ [] m = pure m
foldEffects f update snk (e:es) o =
case runEffect o (f (update e)) of
(n, subs) -> do
forM_ subs $ \sub ->
sub snk `catch` (void . exception)
foldEffects f update snk es n
sink
:: MisoString
-> App effect model action a
-> Sink action
sink name _ = \a ->
M.lookup name <$> liftIO (readIORef componentMap) >>= \case
Just ComponentState {..} -> componentSink a
Nothing -> pure ()
mail
:: Component effect model action a
-> action
-> JSM ()
mail (Component _ name _) action = do
dispatch <- liftIO (readIORef componentMap)
forM_ (M.lookup name dispatch) $ \ComponentState {..} ->
componentSink action
drawComponent
:: Prerender
-> MisoString
-> App effect model action a
-> Sink action
-> JSM (MisoString, JSVal, IORef VTree)
drawComponent prerender name App {..} snk = do
vtree <- runView prerender (view model) snk logLevel events
mountElement <- FFI.getComponent name
when (prerender == DontPrerender) $ diff mountElement Nothing (Just vtree)
ref <- liftIO (newIORef vtree)
pure (name, mountElement, ref)
unmount
:: Function
-> App effect model action a
-> ComponentState model action
-> JSM ()
unmount mountCallback App{..} ComponentState {..} = do
undelegator componentMount componentVTree events (logLevel `elem` [DebugEvents, DebugAll])
freeFunction mountCallback
liftIO (mapM_ killThread componentSubThreads)
liftIO $ do
killThread componentMainThread
modifyIORef' componentMap (M.delete componentName)
runView
:: Prerender
-> View action
-> Sink action
-> LogLevel
-> Events
-> JSM VTree
runView prerender (Embed attributes (SomeComponent (Component key mount app))) snk _ _ = do
mountCallback <- do
FFI.syncCallback1 $ \continuation -> do
vtreeRef <- initialize app (drawComponent prerender mount app)
VTree vtree <- liftIO (readIORef vtreeRef)
void $ call continuation global [vtree]
unmountCallback <- toJSVal =<< do
FFI.syncCallback $ do
M.lookup mount <$> liftIO (readIORef componentMap) >>= \case
Nothing -> pure ()
Just componentState -> do
liftIO (threadDelay (millis 1))
unmount mountCallback app componentState
vcomp <- createNode "vcomp" HTML key "div"
setAttrs vcomp attributes snk (logLevel app) (events app)
flip (FFI.set "children") vcomp =<< toJSVal ([] :: [MisoString])
FFI.set "data-component-id" mount vcomp
flip (FFI.set "mount") vcomp =<< toJSVal mountCallback
FFI.set "unmount" unmountCallback vcomp
pure (VTree vcomp)
runView prerender (Node ns tag key attrs kids) snk logLevel events = do
vnode <- createNode "vnode" ns key tag
setAttrs vnode attrs snk logLevel events
flip (FFI.set "children") vnode
=<< ghcjsPure . jsval
=<< setKids
pure $ VTree vnode
where
setKids = do
kidsViews <- forM kids $ \kid -> do
VTree (Object vtree) <- runView prerender kid snk logLevel events
pure vtree
ghcjsPure (JSArray.fromList kidsViews)
runView _ (Text t) _ _ _ = do
vtree <- create
FFI.set "type" ("vtext" :: JSString) vtree
FFI.set "text" t vtree
pure $ VTree vtree
runView prerender (TextRaw str) snk logLevel events =
case parseView str of
[] ->
runView prerender (Text (" " :: MisoString)) snk logLevel events
[parent] ->
runView prerender parent snk logLevel events
kids -> do
runView prerender (Node HTML "div" Nothing mempty kids) snk logLevel events
createNode :: MisoString -> NS -> Maybe Key -> MisoString -> JSM Object
createNode typ ns key tag = do
vnode <- create
cssObj <- create
propsObj <- create
eventsObj <- create
FFI.set "css" cssObj vnode
FFI.set "type" typ vnode
FFI.set "props" propsObj vnode
FFI.set "events" eventsObj vnode
FFI.set "ns" ns vnode
FFI.set "tag" tag vnode
FFI.set "key" key vnode
pure vnode
setAttrs
:: Object
-> [Attribute action]
-> Sink action
-> LogLevel
-> Events
-> JSM ()
setAttrs vnode attrs snk logLevel events =
forM_ attrs $ \case
Property k v -> do
value <- toJSVal v
o <- getProp "props" vnode
FFI.set k value (Object o)
Event attr -> attr snk vnode logLevel events
Style styles -> do
cssObj <- getProp "css" vnode
forM_ (M.toList styles) $ \(k,v) -> do
FFI.set k v (Object cssObj)
parseView :: MisoString -> [View a]
parseView html = reverse (go (parseTree html) [])
where
go [] xs = xs
go (TagLeaf (TagText s) : next) views =
go next (Text s : views)
go (TagLeaf (TagOpen name attrs) : next) views =
go (TagBranch name attrs [] : next) views
go (TagBranch name attrs kids : next) views =
let
attrs' = [ Property key $ A.String (fromMisoString value)
| (key, value) <- attrs
]
newNode =
Node HTML name Nothing attrs' (reverse (go kids []))
in
go next (newNode:views)
go (TagLeaf _ : next) views =
go next views
registerComponent :: MonadIO m => ComponentState model action -> m ()
registerComponent componentState = liftIO
$ modifyIORef' componentMap
$ M.insert (componentName componentState) componentState
millis :: Int -> Int
millis = (*1000)