glazier-react-1.0.0.0: src/Glazier/React/Reactor/Exec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Glazier.React.Reactor.Exec
( ReactorEnv(..)
, mkReactorEnvIO
, startApp
, reactorBackgroundWork
, execReactorCmd
, execMkReactId
, execSetRender
, execMkSubject
, execBookSubjectCleanup
, execGetModel
, execGetElementalRef
, execRerender
, execTickModel
, execRegisterDOMListener
, execRegisterReactListener
, execRegisterMountedListener
, execRegisterRenderedListener
, execRegisterNextRenderedListener
, execRegisterTickedListener
) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.DeepSeq
import Control.Lens
import Control.Lens.Misc
import Control.Monad.Delegate
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Control.Monad.Trans.Cont
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Maybe.Extras
import Control.Monad.Trans.RWS.Strict
import Control.Monad.Trans.State.Strict
import Data.Diverse.Lens
import qualified Data.DList as DL
import Data.Foldable
import Data.IORef
import qualified Data.JSString as J
import Data.Maybe
import Data.Typeable
import qualified GHCJS.Foreign.Callback as J
import qualified GHCJS.Foreign.Callback.Internal as J
import qualified GHCJS.Foreign.Export as J
import qualified GHCJS.Types as J
import Glazier.Command
import Glazier.React.Component
import Glazier.React.Entity
import Glazier.React.EventTarget
import Glazier.React.Gadget
import Glazier.React.Markup
import Glazier.React.ReactDOM
import Glazier.React.ReactId.Internal
import Glazier.React.Reactor
import Glazier.React.ReadIORef
import Glazier.React.Scene
import Glazier.React.Subject
import Glazier.React.Subject.Internal
import Glazier.React.Widget
import Glazier.React.Window
import qualified JavaScript.Extras as JE
#if MIN_VERSION_base(4,9,0) && !MIN_VERSION_base(4,10,0)
import Data.Semigroup
#endif
data ReactorEnv = ReactorEnv
{ reactIdEnv :: MVar Int
, reactorBackgroundEnv :: TQueue (IO (IO ()))
}
makeLenses_ ''ReactorEnv
mkReactorEnvIO :: IO (ReactorEnv)
mkReactorEnvIO = ReactorEnv <$> (newMVar (0 :: Int)) <*> newTQueueIO
-- | An example of starting an app using the glazier-react framework
startApp ::
( MonadIO m
, MonadReader r m
, Has ReactorEnv r
, Typeable s -- for J.export
, AsReactor cmd
, AsFacet (IO cmd) cmd
)
=> (cmd -> m ()) -> Widget cmd s s () -> s -> JE.JSRep -> m ()
startApp executor wid s root = do
-- background worker thread
q <- view ((hasLens @ReactorEnv)._reactorBackgroundEnv)
liftIO $ void $ forkIO $ forever $ reactorBackgroundWork q
-- create a mvar to store the app subject
sbjVar <- liftIO $ newEmptyMVar
let setup = do
sbj <- mkSubject' wid s
exec' (command_ <$> (putMVar sbjVar sbj))
cs = (`execState` mempty) $ evalContT setup
-- run the initial commands, this will store the app Subject into sbjVar
traverse_ executor cs
-- Start the App render
liftIO $ do
sbj <- takeMVar sbjVar
markup <- unReadIORef $ (`execStateT` mempty) $ displaySubject sbj
e <- toElement markup
renderDOM e root
-- Export sbj to prevent it from being garbage collected
void $ J.export sbj
reactorBackgroundWork :: TQueue (IO (IO ())) -> IO ()
reactorBackgroundWork q = do
-- wait until there is data
x <- atomically $ readTQueue q
-- run the action - this might add more data into the queue
y <- x
-- keep looping until there is no more data in the queue
ys <- go (DL.singleton y)
-- Run the secondary actions - which should rerender
fold ys
where
go zs = do
xs <- atomically $ flushTQueue q
case xs of
[] -> pure zs
xs' -> do
-- run the action - this might add more data into the queue
ys <- sequence xs'
go (zs <> DL.fromList ys)
execReactorCmd ::
( MonadUnliftIO m
, MonadReader r m
, AsReactor cmd
, Has ReactorEnv r
)
=> (cmd -> m ()) -> ReactorCmd cmd -> m ()
execReactorCmd executor c = case c of
MkReactId n k -> execMkReactId n >>= (executor . k)
SetRender sbj w -> execSetRender sbj w
MkSubject wid s k -> execMkSubject executor wid s >>= (executor . k)
-- GetScene sbj k -> execGetScene sbj >>= (executor . k)
GetModel sbj k -> execGetModel sbj >>= (executor . k)
GetElementalRef sbj ri k -> execGetElementalRef executor sbj ri k
-- TickScene sbj tick -> execTickScene sbj tick >>= executor
Rerender sbj -> execRerender sbj
TickModel sbj tick -> execTickModel sbj tick >>= executor
BookSubjectCleanup sbj -> execBookSubjectCleanup sbj
RegisterDOMListener sbj j n goStrict goLazy -> execRegisterDOMListener executor sbj j n goStrict goLazy
RegisterReactListener sbj ri n goStrict goLazy -> execRegisterReactListener executor sbj ri n goStrict goLazy
RegisterMountedListener sbj k -> execRegisterMountedListener executor sbj k
RegisterRenderedListener sbj k -> execRegisterRenderedListener executor sbj k
RegisterNextRenderedListener sbj k -> execRegisterNextRenderedListener executor sbj k
RegisterTickedListener sbj k -> execRegisterTickedListener executor sbj k
-----------------------------------------------------------------
execMkReactId ::
( MonadIO m
, Has ReactorEnv r
, MonadReader r m
)
=> J.JSString
-> m ReactId
execMkReactId n = do
v <- view ((hasLens @ReactorEnv)._reactIdEnv)
liftIO $ do
i <- takeMVar v
let i' = JE.safeIncrement i
putMVar v i'
pure . ReactId . J.append n . J.cons ':' . J.pack $ show i'
doRender :: IORef (Scene s) -> Window s () -> IO J.JSVal
doRender scnRef win = do
-- render using from scnRef (doesn't block)
scn <- readIORef scnRef
(mrkup, _) <- unReadIORef (execRWST win scn mempty) -- ignore unit writer output
a <- JE.toJS <$> toElement mrkup
pure a
doRef :: IORef (Scene s) -> MVar (Scene s) -> J.JSVal -> IO ()
doRef scnRef scnVar j = do
-- update componentRef held in the Plan
scn <- takeMVar scnVar
let scn' = scn & _plan._componentRef .~ (JE.fromJS j)
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
doRendered :: IORef (Scene s) -> MVar (Scene s) -> IO ()
doRendered scnRef scnVar = do
-- update nextRenderedListener held in the Plan
scn <- takeMVar scnVar
let scn' = scn & _plan._nextRenderedListener .~ mempty
nxt = scn ^. _plan._nextRenderedListener
cb = scn ^. _plan._renderedListener
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
nxt
cb
doMounted :: IORef (Scene s) -> IO ()
doMounted scnRef = do
scn <- readIORef scnRef
scn ^. _plan._mountedListener
execSetRender :: MonadIO m => Subject s -> Window s () -> m ()
execSetRender sbj win = liftIO $ do
-- create the callbacks
renderCb <- J.syncCallback' (doRender scnRef win)
-- Create automatic garbage collection of the callbacks
-- that will run when the Subject lease members are garbage collected.
renderLease <- liftIO $ newEmptyMVar
void $ mkWeakMVar renderLease $ J.releaseCallback renderCb
-- Replace the existing ShimCallbacks (was fake version)
scn <- takeMVar scnVar
-- replace the rendering function
atomicWriteIORef rndrLeaseRef renderLease
let scn' = scn & _plan._shimCallbacks._shimRender .~ renderCb
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
rndrLeaseRef = renderLeaseRef sbj
-- | Make an initialized 'Subject' for a given model using the given
-- 'Window' rendering function.
-- The original window should be dropped and the 'Widget' reduced to just a
-- 'Gadget' to emphasis the fact that the 'Window' was used up.
-- 'displaySubject' should be used to render the subject.
execMkSubject ::
( MonadIO m
, AsReactor cmd
, Has ReactorEnv r
, MonadReader r m
)
=> (cmd -> m ())
-> Widget cmd s s ()
-> s
-> m (Subject s)
execMkSubject executor wid s = do
ri <- execMkReactId (J.pack "plan")
(sbj, cs) <- liftIO $ do
-- create shim with fake callbacks for now
let newPlan = Plan
ri
Nothing
(ShimCallbacks (J.Callback J.nullRef) (J.Callback J.nullRef) (J.Callback J.nullRef) (J.Callback J.nullRef))
mempty
mempty
mempty
mempty
mempty
mempty
mempty
False
False
scn = Scene newPlan s
scnRef <- newIORef scn
scnVar <- newEmptyMVar
-- Create a MVar just for auto cleanup of the callbacks
-- This mvar must not be reachable from the callbacks,
-- otherwise the callbacks will always be alive.
otherCbLease <- newEmptyMVar
renderLease <- newEmptyMVar
rndrLeaseRef <- newIORef renderLease
-- Create callbacks for now
renderCb <- J.syncCallback' (pure J.nullRef) -- dummy render for now
refCb <- J.syncCallback1 J.ContinueAsync (doRef scnRef scnVar)
mountedCb <- J.syncCallback J.ContinueAsync (doMounted scnRef)
renderedCb <- J.syncCallback J.ContinueAsync (doRendered scnRef scnVar)
-- Create automatic garbage collection of the callbacks
-- that will run when the Subject lease members are garbage collected.
void $ mkWeakMVar otherCbLease $ do
scn' <- readIORef scnRef
-- scn' ^. _plan._nextRenderedListener
scn' ^. _plan._finalCleanup
-- cleanup callbacks
traverse_ (traverse (J.releaseCallback . fst) . reactListeners) (scn' ^. _plan._elementals)
traverse_ (J.releaseCallback . fst) (scn' ^. _plan._domlListeners)
J.releaseCallback refCb
J.releaseCallback renderedCb
void $ mkWeakMVar renderLease $ J.releaseCallback renderCb
-- Now we have enough to make a subject
let sbj = Subject scnRef scnVar rndrLeaseRef otherCbLease
-- initalize the subject using the Gadget
gad = runExceptT wid
gad' = gad `bindLeft` (exec' . SetRender sbj)
gad'' = (either id id) <$> gad'
tick = runGadget gad'' (Entity sbj id) pure
cs = execState tick mempty
-- update the scene to include the real shimcallbacks
scn' = scn & _plan._shimCallbacks .~ ShimCallbacks renderCb mountedCb renderedCb refCb
-- update the mutable variables with the initialzed scene
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
pure (sbj, cs)
-- execute additional commands
-- one of these commands will be 'SetRender' which will
-- update the dummy render with the real render function.
executor (command' $ DL.toList cs)
-- return the subject
pure sbj
execBookSubjectCleanup ::
( MonadIO m
, MonadReader r m
, Has ReactorEnv r
)
=> Subject s -> m ()
execBookSubjectCleanup sbj = do
liftIO $ do
scn <- takeMVar scnVar
let cleanup = prolong sbj
scn' = scn & _plan._nextRenderedListener %~ (*> cleanup)
-- Update the back buffer
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
-- Trigger a rerender
execRerender sbj
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
execGetModel ::
MonadIO m
=> Subject s
-> m s
execGetModel sbj = liftIO . fmap model . readIORef $ sceneRef sbj
execRerender ::
( MonadIO m
, MonadReader r m
, Has ReactorEnv r
)
=> Subject s -> m ()
execRerender sbj = do
q <- view ((hasLens @ReactorEnv)._reactorBackgroundEnv)
liftIO $ do
scn <- takeMVar scnVar
if not (scn ^. _plan._rerenderRequired)
then do
let scn' = scn & _plan._rerenderRequired .~ True
-- Update the back buffer
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
atomically $ writeTQueue q (pure (scheduleRerender sbj))
-- rerender has already been scheduled
else putMVar scnVar scn
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
scheduleRerender :: Subject s -> IO ()
scheduleRerender sbj = do
scn <- takeMVar scnVar
if scn ^. _plan._rerenderRequired
then do
let scn' = scn & _plan._rerenderRequired .~ False
& _plan._tickedNotified .~ False
-- Update the back buffer
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
case scn ^. _plan._componentRef of
Nothing -> pure ()
Just j -> rerenderShim j
-- rerender not required (eg. already processed)
else putMVar scnVar scn
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
-- | No need to run in a separate thread because it should never block for a significant amount of time.
-- Update the scene 'MVar' with the given action. Also triggers a rerender.
execTickModel ::
( MonadIO m
, MonadReader r m
, Has ReactorEnv r
)
=> Subject s
-> ModelState s cmd
-> m cmd
execTickModel sbj tick = do
q <- view ((hasLens @ReactorEnv)._reactorBackgroundEnv)
liftIO $ do
scn <- takeMVar scnVar
let s = scn ^. _model
(c, s') <- unReadIORef $ runStateT tick s
let scn' = scn & _model .~ s'
-- Update the back buffer
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
atomically $ writeTQueue q notifyTicked
pure c
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
notifyTicked = do
scn <- takeMVar scnVar
if not (scn ^. _plan._tickedNotified)
then do
let scn' = scn & _plan._tickedNotified .~ True
& _plan._rerenderRequired .~ True
cb = scn ^. _plan._tickedListener
-- Update the back buffer
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
-- run tickedListeener
cb
pure (scheduleRerender sbj)
-- notify not required (eg. already processed)
else do
putMVar scnVar scn
pure (pure ())
mkEventCallback ::
(MonadIO m)
=> IORef (J.JSVal -> IO (), IO ())
-> m (J.Callback (J.JSVal -> IO ()))
mkEventCallback hdlRef = do
liftIO $ J.syncCallback1 J.ContinueAsync $ \evt -> do
(preprocessor, postprocessor) <- readIORef hdlRef
-- first run all the non-blocking preprocessors
preprocessor evt
-- then run all the possibly blocking postprocessors
postprocessor
-- | Using the NFData idea from React/Flux/PropertiesAndEvents.hs
-- React re-uses Notice from a pool, which means it may no longer be valid if we lazily
-- parse it. However, we still want lazy parsing so we don't parse unnecessary fields.
-- Additionally, we don't want to block during the event handling.The reason this is a problem is
-- because Javascript is single threaded, but Haskell is lazy.
-- Therefore GHCJS threads are a strange mixture of synchronous and asynchronous threads,
-- where a synchronous thread might be converted to an asynchronous thread if a "black hole" is encountered.
-- See https://github.com/ghcjs/ghcjs-base/blob/master/GHCJS/Concurrent.hs
-- This safe interface requires two input functions:
-- 1. a function to reduce Notice to a NFData. The handleEvent will ensure that the
-- NFData is forced which will ensure all the required fields from Synthetic event has been parsed.
-- This function must not block.
-- 2. a second function that uses the NFData. This function is allowed to block.
-- handleEvent results in a function that you can safely pass into 'GHC.Foreign.Callback.syncCallback1'
-- with 'GHCJS.Foreign.Callback.ContinueAsync'.
-- I have innovated further with the NFData idea to return two functions:
-- 1. (evt -> IO ()) function to preprocess the event, which is guaranteed to be non blocking.
-- 2. An IO () postprocessor function which may block.
-- This allows for multiple handlers for the same event to be processed safely,
-- by allowing a way for all the preprocessor handlers are all run first before
-- running all of the postprocessor handlers.
mkEventHandler :: (NFData a) => (evt -> MaybeT IO a) -> IO (evt -> IO (), MaybeT IO a)
mkEventHandler goStrict = do
-- create a channel to write preprocessed data for the postprocessor
-- 'Chan' guarantees that the writer is never blocked by the reader.
-- There is only one reader/writer per channel.
c <- newTQueueIO
let preprocess evt = (`evalMaybeT` ()) $ do
r <- goStrict evt
-- This is guaranteed never to block
lift $ atomically $ writeTQueue c $!! r
-- there might not be a value in the chan
-- because the preprocessor might not have produced any values
postprocess = MaybeT $ atomically $ tryReadTQueue c
pure (preprocess, postprocess)
addEventHandler :: (NFData a)
=> (JE.JSRep -> MaybeT IO a)
-> (a -> IO ())
-> IORef (J.JSVal -> IO (), IO ())
-> IO ()
addEventHandler goStrict goLazy listenerRef = do
-- update the ioref with the new handler
(preprocessor, postprocessor) <- mkEventHandler (goStrict . JE.toJSR)
let postprocessor' = (`evalMaybeT` ()) (postprocessor >>= (lift . goLazy))
atomicModifyIORef' listenerRef $ \hdl -> (hdl `mappendListener` (preprocessor, postprocessor'), ())
-- | Create ref handler to assign 'elementalRef'
data Freshness = Existing | Fresh
execGetElementalRef ::
( MonadUnliftIO m
, MonadReader r m
, Has ReactorEnv r
)
=> (cmd -> m ())
-> Subject s
-> ReactId
-> (EventTarget -> cmd)
-> m ()
execGetElementalRef executor sbj ri k = do
UnliftIO u <- askUnliftIO
scn <- liftIO $ takeMVar scnVar
(refFreshness, pln) <- registerRefCoreListener sbj ri (plan scn)
let tryAgain = u $ execGetElementalRef executor sbj ri k
pln' = pln & _nextRenderedListener %~ (*> tryAgain)
scn' = scn & _plan .~ pln'
ret = pln ^? (_elementals.ix ri._elementalRef._Just)
doTryAgain = do
liftIO $ do
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
-- trigger a rerender
execRerender sbj
case refFreshness of
Fresh -> doTryAgain
Existing -> case ret of
Nothing -> doTryAgain
Just ret' -> do
liftIO $ putMVar scnVar scn
executor . k $ ret'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
-- | This one doesn't take the sceneVar
registerRefCoreListener :: (MonadIO m)
=> Subject s
-> ReactId
-> Plan
-> m (Freshness, Plan)
registerRefCoreListener sbj ri pln = do
liftIO $ do
-- first get or make the target
(freshness, eventHdl) <-
case pln ^. _elementals.at ri.to (fromMaybe (Elemental Nothing mempty))._reactListeners.at n of
Nothing -> do
listenerRef <- newIORef mempty
cb <- mkEventCallback listenerRef
-- update listenerRef with new event listener
-- only do this once (when for first ref listener)
addEventHandler (pure . JE.fromJSR) hdlRef listenerRef
pure (Fresh, (cb, listenerRef))
Just eventHdl -> pure (Existing, eventHdl)
-- prepare the updated state
let pln' = pln & _elementals.at ri %~ (Just . addElem . initElem)
initElem = fromMaybe (Elemental Nothing mempty)
addElem = _reactListeners.at n %~ addListener
addListener = Just . maybe eventHdl (const eventHdl)
case freshness of
Fresh -> pure (Fresh, pln')
Existing -> pure (Existing, pln)
where
n = J.pack "ref"
-- hdlRef x = command' $ TickScene sbj (command_ <$> (_plan._elementals.ix ri._elementalRef .= x))
scnRef = sceneRef sbj
scnVar = sceneVar sbj
hdlRef x = do
scn <- takeMVar scnVar
let scn' = scn & _plan._elementals.ix ri._elementalRef .~ x
-- Update the back buffer
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
execRegisterReactListener :: (NFData a, MonadUnliftIO m)
=> (cmd -> m ())
-> Subject s
-> ReactId
-> J.JSString
-> (JE.JSRep -> MaybeT IO a)
-> (a -> cmd)
-> m ()
execRegisterReactListener executor sbj ri n goStrict goLazy = do
UnliftIO u <- askUnliftIO
scn_ <- liftIO $ takeMVar scnVar
-- special logic for ref, where the first ref handler must be 'registerRefCoreListener'
scn <- if (n == (J.pack "ref"))
then do
(refFreshness, pln) <- registerRefCoreListener sbj ri (plan scn_)
case refFreshness of
Existing -> pure scn_
Fresh -> pure (scn_ & _plan .~ pln)
else pure scn_
liftIO $ do
-- get or make the target
(freshness, eventHdl@(_, listenerRef)) <-
case scn ^. _plan._elementals.at ri.to (fromMaybe (Elemental Nothing mempty))._reactListeners.at n of
Nothing -> do
listenerRef <- newIORef mempty
cb <- mkEventCallback listenerRef
pure (Fresh, (cb, listenerRef))
Just eventHdl -> pure (Existing, eventHdl)
let scn' = case freshness of
Fresh -> scn & _plan._elementals.at ri %~ (Just . addElem . initElem)
Existing -> scn
initElem = fromMaybe (Elemental Nothing mempty)
addElem = _reactListeners.at n %~ addListener
addListener = Just . maybe eventHdl (const eventHdl)
-- update listenerRef with new event listener
addEventHandler goStrict (u . executor . goLazy) listenerRef
-- Update the subject
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
mappendListener :: (J.JSVal -> IO (), IO ()) -> (J.JSVal -> IO (), IO ()) -> (J.JSVal -> IO (), IO ())
mappendListener (f1, g1) (f2, g2) = (\x -> f1 x *> f2 x, g1 *> g2)
execRegisterTickedListener :: (MonadUnliftIO m)
=> (cmd -> m ())
-> Subject s
-> cmd
-> m ()
execRegisterTickedListener executor sbj c = do
UnliftIO u <- askUnliftIO
let hdl = u $ executor c
liftIO $ do
scn <- takeMVar scnVar
let scn' = scn & _plan._tickedListener %~ (*> hdl)
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
execRegisterMountedListener :: (MonadUnliftIO m)
=> (cmd -> m ())
-> Subject s
-> cmd
-> m ()
execRegisterMountedListener executor sbj c = do
UnliftIO u <- askUnliftIO
let hdl = u $ executor c
liftIO $ do
scn <- takeMVar scnVar
let scn' = scn & _plan._mountedListener %~ (*> hdl)
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
execRegisterRenderedListener :: (MonadUnliftIO m)
=> (cmd -> m ())
-> Subject s
-> cmd
-> m ()
execRegisterRenderedListener executor sbj c = do
UnliftIO u <- askUnliftIO
let hdl = u $ executor c
liftIO $ do
scn <- takeMVar scnVar
let scn' = scn & _plan._renderedListener %~ (*> hdl)
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
execRegisterNextRenderedListener :: (MonadUnliftIO m)
=> (cmd -> m ())
-> Subject s
-> cmd
-> m ()
execRegisterNextRenderedListener executor sbj c = do
UnliftIO u <- askUnliftIO
let hdl = u $ executor c
liftIO $ do
scn <- takeMVar scnVar
let scn' = scn & _plan._nextRenderedListener %~ (*> hdl)
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
execRegisterDOMListener ::
( NFData a
, MonadUnliftIO m
, Has ReactorEnv r
, MonadReader r m
)
=> (cmd -> m ())
-> Subject s
-> JE.JSRep
-> J.JSString
-> (JE.JSRep -> MaybeT IO a)
-> (a -> cmd)
-> m ()
execRegisterDOMListener executor sbj j n goStrict goLazy = do
-- Add the handler to the state
UnliftIO u <- askUnliftIO
-- generate a unique id
ri <- execMkReactId n
liftIO $ do
scn <- takeMVar scnVar
-- since ri is unique, it'll always be a new map item
-- update the ioref with the new handler
listenerRef <- newIORef mempty
cb <- mkEventCallback listenerRef
addEventHandler goStrict (u . executor . goLazy) listenerRef
-- prepare the updated state,
let scn' = scn & _plan._domlListeners.at ri .~ (Just (cb, listenerRef))
& _plan._finalCleanup %~ (*> removeDomListener j n cb)
-- Update the subject
atomicWriteIORef scnRef scn'
putMVar scnVar scn'
-- now add the domListener to the javascript target
addDomListener j n cb
where
scnRef = sceneRef sbj
scnVar = sceneVar sbj
addDomListener :: JE.JSRep -> J.JSString -> J.Callback (J.JSVal -> IO ()) -> IO ()
addDomListener j n cb = js_addDomListener (JE.toJS j) n (JE.toJS cb)
removeDomListener :: JE.JSRep -> J.JSString -> J.Callback (J.JSVal -> IO ()) -> IO ()
removeDomListener j n cb = js_removeDomListener (JE.toJS j) n (JE.toJS cb)
#ifdef __GHCJS__
foreign import javascript unsafe
"if ($1 && $1['addEventListener']) { $1['addEventListener']($2, $3); }"
js_addDomListener :: J.JSVal -> J.JSString -> J.JSVal -> IO ()
foreign import javascript unsafe
"if ($1 && $1['removeEventListener']) { $1['removeEventListener']($2, $3); }"
js_removeDomListener :: J.JSVal -> J.JSString -> J.JSVal -> IO ()
#else
js_addDomListener :: J.JSVal -> J.JSString -> J.JSVal -> IO ()
js_addDomListener _ _ _ = pure mempty
js_removeDomListener :: J.JSVal -> J.JSString -> J.JSVal -> IO ()
js_removeDomListener _ _ _ = pure mempty
#endif