Dynamic

Dynamic’s are essential to support Hot Reload.

A Dynamic is simply defined as:

newtype Dynamic m a
  = Dynamic
      ( -- Initial value
        a
      , -- Set a new value
        (a -> m ()) -> m ()
      )

It is a pair of values: the initial value, and a function that knows how to update that value over time using the user-provided update function (a -> m ()). Dynamic’s are an Applicative, so they compose using liftA* family of functions.

The EmaSite’s siteInput method returns a Dynamic of Model type, which represents all the data required to render a site. If you do not want Hot Reload, you may return a pure value.

The use of a time-varying Dynamic is what enables Hot Reload. See here for an example of making a model time-varying. Checkout unionmount to produce a Dynamic of a model that updates based on the filesystem tree.

Reading the current value from outside

Dynamic is push-only: its updater hands each new value to a callback the consumer supplies. That shape fits Ema’s render loop, but not a second component that wants synchronous pull access to the latest value — e.g. an HTTP handler running alongside runSiteWith that needs the current model per request.

Ema.Dynamic.currentValue tees a Dynamic for that case:

currentValue :: MonadIO m => Dynamic m a -> m (IO a, Dynamic m a)

It returns a reader (IO a, yielding the most recently pushed value, or the initial value before any update) and a pass-through Dynamic that must be used in place of the input — the pass-through’s updater is wired to feed the reader on each update. Only one producer runs; the pass-through intercepts the send callback, it does not duplicate the underlying updater.

To compose this with Ema’s own render loop, use Ema.App.runSiteWithInput, which takes a pre-built Dynamic instead of calling siteInput for you:

flip runLoggerLoggingT logger $ do
  rawDyn            <- siteInput @r action arg     -- your EmaSite.siteInput runs
  (readModel, dyn') <- currentValue rawDyn         -- tee for out-of-band reads
  race_
    (myObserver readModel)                          -- whatever needs the live model
    (runSiteWithInput cfg dyn')                     -- Ema drives the wrapped Dynamic

UnliftIO.Async.race_ handles both branches in m directly — no withRunInIO/liftIO dance needed. runSiteWith itself becomes a one-liner wrapper around siteInput + runSiteWithInput, so callers that don’t need the tee are unaffected.