reflex-0.7.2.0: src/Reflex/Host/Headless.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
module Reflex.Host.Headless where
import Control.Concurrent.Chan (newChan, readChan)
import Control.Monad (unless)
import Control.Monad.Fix (MonadFix, fix)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Primitive (PrimMonad)
import Control.Monad.Ref (MonadRef, Ref, readRef)
import Data.Dependent.Sum (DSum (..), (==>))
import Data.Foldable (for_)
import Data.Functor.Identity (Identity(..))
import Data.IORef (IORef, readIORef)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Traversable (for)
import Reflex
import Reflex.Host.Class
type MonadHeadlessApp t m =
( Adjustable t m
, MonadFix m
, MonadHold t m
, MonadIO (HostFrame t)
, MonadIO (Performable m)
, MonadIO m
, MonadRef (HostFrame t)
, NotReady t m
, PerformEvent t m
, PostBuild t m
, PrimMonad (HostFrame t)
, Ref (HostFrame t) ~ IORef
, Ref m ~ IORef
, Reflex t
, ReflexHost t
, TriggerEvent t m
)
-- | Run a headless FRP network. Inside the action, you will most probably use
-- the capabilities provided by the 'TriggerEvent' and 'PerformEvent' type
-- classes to interface the FRP network with the outside world. Useful for
-- testing. Each headless network runs on its own spider timeline.
runHeadlessApp
:: (forall t m. MonadHeadlessApp t m => m (Event t ()))
-- ^ The action to be run in the headless FRP network. The FRP network is
-- closed at the first occurrence of the resulting 'Event'.
-> IO ()
runHeadlessApp guest =
-- We are using the 'Spider' implementation of reflex. Running the host
-- allows us to take actions on the FRP timeline.
withSpiderTimeline $ runSpiderHostForTimeline $ do
-- Create the "post-build" event and associated trigger. This event fires
-- once, when the application starts.
(postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
-- Create a queue to which we will write 'Event's that need to be
-- processed.
events <- liftIO newChan
-- Run the "guest" application, providing the appropriate context. We'll
-- pure the result of the action, and a 'FireCommand' that will be used to
-- trigger events.
(result, fc@(FireCommand fire)) <- do
hostPerformEventT $ -- Allows the guest app to run
-- 'performEvent', so that actions
-- (e.g., IO actions) can be run when
-- 'Event's fire.
flip runPostBuildT postBuild $ -- Allows the guest app to access to
-- a "post-build" 'Event'
flip runTriggerEventT events $ -- Allows the guest app to create new
-- events and triggers and write
-- those triggers to a channel from
-- which they will be read and
-- processed.
guest
-- Read the trigger reference for the post-build event. This will be
-- 'Nothing' if the guest application hasn't subscribed to this event.
mPostBuildTrigger <- readRef postBuildTriggerRef
-- Subscribe to an 'Event' of that the guest application can use to
-- request application shutdown. We'll check whether this 'Event' is firing
-- to determine whether to terminate.
shutdown <- subscribeEvent result
-- When there is a subscriber to the post-build event, fire the event.
soa <- for mPostBuildTrigger $ \postBuildTrigger ->
fire [postBuildTrigger :=> Identity ()] $ isFiring shutdown
-- The main application loop. We wait for new events and fire those that
-- have subscribers. If we detect a shutdown request, the application
-- terminates.
unless (or (fromMaybe [] soa)) $ fix $ \loop -> do
-- Read the next event (blocking).
ers <- liftIO $ readChan events
stop <- do
-- Fire events that have subscribers.
fireEventTriggerRefs fc ers $
-- Check if the shutdown 'Event' is firing.
isFiring shutdown
if or stop
then pure ()
else loop
where
isFiring ev = readEvent ev >>= \case
Nothing -> pure False
Just _ -> pure True
-- Use the given 'FireCommand' to fire events that have subscribers
-- and call the callback for the 'TriggerInvocation' of each.
fireEventTriggerRefs
:: MonadIO m
=> FireCommand t m
-> [DSum (EventTriggerRef t) TriggerInvocation]
-> ReadPhase m a
-> m [a]
fireEventTriggerRefs (FireCommand fire) ers rcb = do
mes <- liftIO $
for ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do
me <- readIORef er
pure $ fmap (==> a) me
a <- fire (catMaybes mes) rcb
liftIO $ for_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
pure a