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.