reflex-vty-1.0.0.0: src/Reflex/Vty/Host/Trigger.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module: Reflex.Vty.Host.Trigger
-- Description: A bounded 'TriggerEvent' layer that backpressures hot producers
--
-- This is an internal module used by "Reflex.Vty.Host". It provides a drop-in
-- replacement for reflex's @'Reflex.TriggerEvent.Base.TriggerEventT'@ whose
-- underlying queue is bounded and closeable. External triggers
-- ('Reflex.performEventAsync', 'Reflex.newTriggerEvent', etc.) write into a
-- fixed-capacity queue, so a producer that fires faster than the host can
-- process is throttled (its @fire@ callback blocks when the queue is full)
-- rather than allowed to exhaust memory. Because the producer's own write
-- blocks, no event occurrences are dropped.
--
-- The queue is also closeable: on shutdown the host closes it, which releases
-- any producer currently blocked on a full queue.
module Reflex.Vty.Host.Trigger
( -- * Bounded event queue
BoundedEventQueue
, newBoundedEventQueue
, closeBoundedEventQueue
, writeBoundedEventQueue
, drainBoundedEventQueue
-- * TriggerEvent transformer
, BoundedTriggerT
, runBoundedTriggerT
) where
import Control.Concurrent.STM
( STM
, TBQueue
, TMVar
, atomically
, flushTBQueue
, newEmptyTMVarIO
, newTBQueueIO
, orElse
, readTBQueue
, readTMVar
, tryPutTMVar
, writeTBQueue
)
import Control.Monad (void)
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
import Control.Monad.Fix (MonadFix)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Morph (MFunctor (hoist))
import Control.Monad.Primitive (PrimMonad (..))
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Monad.Ref (MonadAtomicRef (..), MonadRef (..), Ref)
import Control.Monad.Trans (MonadTrans, lift)
import Data.Coerce (coerce)
import Data.Dependent.Sum (DSum ((:=>)))
import Numeric.Natural (Natural)
import Reflex
import Reflex.Host.Class (MonadReflexCreateTrigger (..), newEventWithTriggerRef)
-- | A pending batch of trigger invocations. Each external @fire@ enqueues one
-- such batch (almost always a singleton).
type Batch t = [DSum (EventTriggerRef t) TriggerInvocation]
-- | A bounded, closeable queue of pending trigger invocations.
--
-- * Writes ('writeBoundedEventQueue') block when the queue is full
-- (backpressure to the producer).
-- * Once closed, writes return immediately without enqueueing, so producers
-- blocked at shutdown are released.
-- * Reads ('drainBoundedEventQueue') block until at least one batch is
-- available (or the queue is closed), then drain everything currently
-- queued.
data BoundedEventQueue t = BoundedEventQueue
{ _beqQueue :: !(TBQueue (Batch t))
, _beqClosed :: !(TMVar ())
-- ^ Full when the queue has been closed.
}
-- | Create a bounded event queue with the given capacity. Capacity must be at
-- least 1; smaller values are clamped to 1.
newBoundedEventQueue :: Natural -> IO (BoundedEventQueue t)
newBoundedEventQueue cap =
BoundedEventQueue
<$> newTBQueueIO (max 1 cap)
<*> newEmptyTMVarIO
-- | Close a queue. Idempotent. After this call, blocked and future writers
-- return immediately without enqueueing.
closeBoundedEventQueue :: BoundedEventQueue t -> IO ()
closeBoundedEventQueue q = atomically $ void $ tryPutTMVar (_beqClosed q) ()
-- | Enqueue a batch, blocking if the queue is full. Becomes a no-op once the
-- queue is closed (so a producer blocked here unblocks on shutdown).
writeBoundedEventQueue :: BoundedEventQueue t -> Batch t -> STM ()
writeBoundedEventQueue q batch =
-- If there is room, write. Otherwise retry until there is room /or/ the
-- queue is closed (in which case silently drop).
writeTBQueue (_beqQueue q) batch `orElse` void (readTMVar (_beqClosed q))
-- | Block until at least one batch is available, then return it together with
-- every other batch currently queued, as a list of distinct batches (NOT
-- concatenated). The host fires each batch in its own frame so that multiple
-- occurrences of the same trigger that landed in the same drain are preserved
-- (Reflex collapses simultaneous same-trigger firings, so firing them as one
-- batch would drop occurrences). Returns 'Nothing' if the queue is closed and
-- empty.
drainBoundedEventQueue :: BoundedEventQueue t -> IO (Maybe [Batch t])
drainBoundedEventQueue q =
atomically $
let readFirst = Just <$> readTBQueue (_beqQueue q)
waitClosed = Nothing <$ readTMVar (_beqClosed q)
in do
mFirst <- readFirst `orElse` waitClosed
case mFirst of
Nothing -> return Nothing
Just first -> do
rest <- flushTBQueue (_beqQueue q)
return $ Just (first : rest)
----------------------------------------------------------------------------
-- TriggerEvent transformer
----------------------------------------------------------------------------
-- | A 'TriggerEvent' implementation that writes external triggers into a
-- 'BoundedEventQueue', applying backpressure when the queue is full. It is a
-- drop-in replacement for @TriggerEventT@ (same instance set); only its
-- 'TriggerEvent' instance differs, routing writes through
-- 'writeBoundedEventQueue'.
newtype BoundedTriggerT t m a = BoundedTriggerT
{ unBoundedTriggerT :: ReaderT (BoundedEventQueue t) m a
}
deriving
( Applicative
, Functor
, Monad
, MonadCatch
, MonadFix
, MonadIO
, MonadMask
, MonadThrow
)
-- | Run a 'BoundedTriggerT' action against a particular 'BoundedEventQueue'.
runBoundedTriggerT :: BoundedTriggerT t m a -> BoundedEventQueue t -> m a
runBoundedTriggerT = runReaderT . unBoundedTriggerT
instance MonadTrans (BoundedTriggerT t) where
lift = BoundedTriggerT . lift
instance MFunctor (BoundedTriggerT t) where
hoist f = BoundedTriggerT . hoist f . unBoundedTriggerT
instance PrimMonad m => PrimMonad (BoundedTriggerT t m) where
type PrimState (BoundedTriggerT t m) = PrimState m
primitive = lift . primitive
instance PerformEvent t m => PerformEvent t (BoundedTriggerT t m) where
type Performable (BoundedTriggerT t m) = Performable m
performEvent_ = lift . performEvent_
performEvent = lift . performEvent
instance PostBuild t m => PostBuild t (BoundedTriggerT t m) where
getPostBuild = lift getPostBuild
instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (BoundedTriggerT t m) where
newEventWithTrigger = lift . newEventWithTrigger
newFanEventWithTrigger f = lift $ newFanEventWithTrigger f
instance NotReady t m => NotReady t (BoundedTriggerT t m) where
notReadyUntil = lift . notReadyUntil
notReady = lift notReady
instance MonadRef m => MonadRef (BoundedTriggerT t m) where
type Ref (BoundedTriggerT t m) = Ref m
newRef = lift . newRef
readRef = lift . readRef
writeRef r = lift . writeRef r
instance MonadAtomicRef m => MonadAtomicRef (BoundedTriggerT t m) where
atomicModifyRef r = lift . atomicModifyRef r
instance MonadSample t m => MonadSample t (BoundedTriggerT t m) where
sample = lift . sample
instance MonadHold t m => MonadHold t (BoundedTriggerT t m) where
hold v0 v' = lift $ hold v0 v'
holdDyn v0 v' = lift $ holdDyn v0 v'
holdIncremental v0 v' = lift $ holdIncremental v0 v'
buildDynamic a0 = lift . buildDynamic a0
headE = lift . headE
now = lift now
instance Adjustable t m => Adjustable t (BoundedTriggerT t m) where
runWithReplace (BoundedTriggerT a0) a' = BoundedTriggerT $ runWithReplace a0 (coerceEvent a')
traverseIntMapWithKeyWithAdjust f dm0 dm' =
BoundedTriggerT $ traverseIntMapWithKeyWithAdjust (coerce . f) dm0 dm'
traverseDMapWithKeyWithAdjust f dm0 dm' =
BoundedTriggerT $ traverseDMapWithKeyWithAdjust (coerce . f) dm0 dm'
traverseDMapWithKeyWithAdjustWithMove f dm0 dm' =
BoundedTriggerT $ traverseDMapWithKeyWithAdjustWithMove (coerce . f) dm0 dm'
instance
( Monad m
, MonadRef m
, Ref m ~ Ref IO
, MonadReflexCreateTrigger t m
)
=> TriggerEvent t (BoundedTriggerT t m)
where
newTriggerEvent = do
(e, t) <- newTriggerEventWithOnComplete
return (e, \a -> t a $ return ())
newTriggerEventWithOnComplete = do
q <- BoundedTriggerT ask
(eResult, reResultTrigger) <- lift newEventWithTriggerRef
return . (,) eResult $ \a cb ->
atomically $ writeBoundedEventQueue q [EventTriggerRef reResultTrigger :=> TriggerInvocation a cb]
newEventWithLazyTriggerWithOnComplete f = do
q <- BoundedTriggerT ask
lift . newEventWithTrigger $ \t ->
f $ \a cb -> do
reResultTrigger <- newRef $ Just t
atomically $ writeBoundedEventQueue q [EventTriggerRef reResultTrigger :=> TriggerInvocation a cb]