packages feed

reflex-vty-1.3.0.0: internal/Reflex/Vty/Host/Trigger.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# 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
-- direct 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. A producer that fires faster than the
-- host can process is throttled: its @fire@ callback blocks when the
-- queue is full. Memory use stays bounded. Because the producer's own
-- write blocks, no event occurrences are dropped.
--
-- The queue is also closeable. On shutdown the host closes it. This
-- releases any producer currently blocked on a full queue.
module Reflex.Vty.Host.Trigger
  ( -- * Bounded event queue
    BoundedEventQueue
  , Batch
  , 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.
--   This applies backpressure to the producer.
-- * Once closed, writes return immediately without enqueueing.
--   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. This becomes a
-- no-op once the queue is closed. A producer blocked here then
-- unblocks on shutdown. Closure is checked first, in the same
-- transaction, so a closed queue refuses writes even when it has
-- spare capacity.
writeBoundedEventQueue :: BoundedEventQueue t -> Batch t -> STM ()
writeBoundedEventQueue q batch =
  void (readTMVar (_beqClosed q)) `orElse` writeTBQueue (_beqQueue q) batch

-- | 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 multiple occurrences of the same trigger that
-- landed in the same drain are preserved. Reflex collapses
-- simultaneous same-trigger firings; 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
-- direct replacement for @TriggerEventT@ with the same instance set. Only its
-- 'TriggerEvent' instance differs. It routes 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]