A block is represented by several (nested) shiny modules and the top level
module is created using the block_server() generic. S3 dispatch is offered
as a way to add flexibility, but in most cases the default method for the
block class should suffice at top level. Further entry points for
customization are offered by the generics expr_server() and block_eval(),
which are responsible for initializing the block "expression" module (i.e.
the block server function passed in new_block()) and block evaluation
(evaluating the interpolated expression in the context of input data),
respectively.
Usage
block_eval(x, expr, env, ...)
eval_env(data)
block_eval_trigger(x, session = get_session())
block_server(id, x, data = list(), ...)
# S3 method for class 'block'
block_server(
id,
x,
data = list(),
block_id = id,
edit_block = NULL,
ctrl_block = NULL,
board = reactiveValues(),
update = reactiveVal(),
inputs_ready = reactive(TRUE),
needed = reactive(TRUE),
visibility = NULL,
...
)
expr_server(x, data, ...)
block_render_trigger(x, session = get_session())Arguments
- x
Object for which to generate a
shiny::moduleServer()- expr
Quoted expression to evaluate in the context of
data- env
Environment in which to evaluate
expr- ...
Generic consistency
- data
Input data (list of reactives)
- session
Shiny session object
- id
Namespace ID
- block_id
Block ID
- edit_block, ctrl_block
Block plugins
- board
Reactive values object containing board information
- update
Reactive value object to initiate board updates
- inputs_ready
Reactive flag signaling whether the block's required inputs are all connected to ready upstream blocks (supplied by
board_server(); defaults to always-ready when a block server is run standalone)- needed
Reactive flag signaling whether the block is currently in the eval set (supplied by
board_server(); defaults to always-needed when a block server is run standalone)- visibility
Front-end channel bundle – a
gatereactiveValholding the owner label of the front-end that made the board lazy, plusvisibleandfrozen, each an environment of per-blockreactiveVals, supplied byboard_server()to hold rendering until a block is painted and to freeze block inputs;NULL(the standalone default) renders the block as soon as it is ready
Value
Both block_server() and expr_server() return shiny server module
(i.e. a call to shiny::moduleServer()), while block_eval() evaluates
an interpolated (w.r.t. block "user" inputs) block expression in the context
of block data inputs.
Details
The module returned from block_server(), at least in the default
implementation, provides much of the essential but block-type agnostic
functionality, including data input validation (if available), instantiation
of the block expression server (handling the block-specific functionality,
i.e. block user inputs and expression), and instantiation of the
edit_block module (if passed from the parent scope).
Each block carries an eval status – one of unevaluated, stale,
waiting, unset, failed or ready – which, together with its
orthogonal front-end visibility, determines its behavior. The status is what
the block's last check found, a check being a run of the block or the
finding that it cannot run. A block that is needed – held eager, or
feeding a block that is, as described below – is checked afresh whenever its
status or result is read. One that is not, a parked block, keeps its inputs
unfulfilled (shiny::req() out) and evaluates nothing: it reports what its
last check found, and its result is the one that check left, for as long as
nothing the check read has changed. Needed or parked, a block that is current
reads the same. Four statuses are what a check can find, separating the two
input kinds (data inputs from links, user inputs from state) and a genuine
failure:
waiting– a required data input is missing: unconnected, below the required number of variadic...argsinputs (one by default), or fed by an upstream block that is not itselfready(seeallow_empty_state).unset– data inputs are ready, but a required user input (statevalue) has not been provided (unless permitted byallow_empty_state).failed– all inputs are present, but the block cannot produce a result: the data validator (validate_data_inputs()) or the block expression raised. The offending condition is surfaced through the block conditions.ready– evaluation succeeded and a result (possibly a legitimateNULL) is available for downstream blocks to consume.
A parked block reads one of the other two when it has no current check to report on:
stale– something the last check read has changed since: the block's expression or eval trigger, which blocks feed its data inputs, or what one of those holds. An upstream that is itselfstaleorunevaluatedcounts as changed, so a change reaches the whole downstream cone. The block is not re-evaluated; the status only reports that what its last check found is out of date, so a front-end can flag it (e.g. a muted node badge) without forcing a recompute. An expression built from the input data cannot be rebuilt while those are withheld, so for such a block thestateit is built from is compared instead.unevaluated– the block has never been checked, which includes a board block that is not built yet.
A consumer that needs a parked block current asks for it with a
board_update() evaluate request. Once none of the blocks it asked for
reads stale or unevaluated, what they report is current.
A block reaches ready only once its upstreams have, so an unconnected or
pending block holds its whole downstream chain waiting without any of them
evaluating against missing data. Output rendering follows the status: the
block output is shown only while ready and cleared otherwise, so a block
leaving ready never displays a stale result. While not ready the block
surfaces a condition explaining why – a status-phase note for waiting
and unset, or the raised error for failed. The note is recorded by the
check that finds the block unable to run, so a block checked off screen
carries it too, and the check that runs the block clears it. Conditions
raised during validation and evaluation are caught and returned to be
surfaced to the app user.
Block-level user inputs (provided by the expression module) are separated
from output, the behavior of which can be customized via the
block_output() generic. The block_ui() generic can then be used to
control rendering of outputs.
A board is eager by default: every block is needed, so every block
evaluates. A front-end (such as blockr.dock) makes it lazy by returning
eager() from the callback it registers with board_server(), naming its
owner label and the blocks it needs evaluated from the start. From then on
only the blocks some owner holds eager are needed, together with what feeds
them. A board whose callbacks return no such value stays eager, and setting
the gate_visibility blockr_option() (default TRUE) to FALSE keeps
every board eager. Core reads the declaration as it runs the callbacks and
seeds the opening eager set there and then, before the first flush decides
what to construct – which no board update could do, since a payload only
applies at the end of the flush it is written in.
Which blocks the front-end needs evaluated from then on is not a channel of
its own: it travels as an eager component under that same owner label,
leaving the front-end one owner among several rather than a special case
core can distinguish from a code export or an extension (see the Evaluation
requests section of board_server()).
Evaluation follows the needed set, the blocks held eager together with
their upstream closure over board_links() (recomputed only when eager sets
or links change). A block's input data reactives stay unfulfilled (they
shiny::req() out) unless the block is needed, so a block that is neither
held eager nor feeding one pulls no input and stays fully quiescent: its
result reactive hands back what the last check left, and any observer its
expression server registers on the incoming data short-circuits and does
nothing. A needed but off-screen block (one feeding a block held eager)
evaluates but does not render.
Rendering follows visible, the per-block channel through which the
front-end reports what it has painted – the effect, where holding a block
eager is the cause. The render observer is suspended while a block carries
no visible slot and resumed once the front-end reports it painted, starting
suspended so nothing renders before the first report.
Block-server construction is prioritized the same way: the needed set is
instantiated first so that first paint waits only for the blocks held eager
and their upstreams, and the remaining block servers are built progressively
in the background. That background pass holds until the front-end reports
every block it holds eager as visible, so it never competes with first paint.
Until a block is built it is absent from the board$blocks handed to plugins
and callbacks, which simply see it appear once constructed. The background
cadence is set by the background_construction_delay blockr_option()
(milliseconds between successive blocks, default 50); a value of 0 disables
the staggering and builds every block up front.
Core's own board UI drives those channels through a callback, on the same
footing as a front-end rather than built into the board server. Stacks
render as a bslib::accordion() which opens one stack and collapses the
rest (see stack_ui()), so on a stacked board part of what is on screen is
hidden from the first render and any stack can be collapsed afterwards.
The gate_stacks() callback reads that accordion back, holding the blocks
of every open stack plus every unstacked block eager and parking the rest,
so collapsing a stack stops its blocks evaluating and expanding one starts
them again. It is board_server()'s default callbacks value. Which stacks
render open is core's own decision (see stack_ui()), so on a stacked board
the callback returns that set as its opening eager set: an eager board
evaluates every block, and a collapsed stack's blocks would otherwise
evaluate once before the accordion reports. The accordion's report then
refines the set rather than establishing it. A board with no stacks binds no
such input and has nothing to park, so this callback leaves it eager; a
board driven by another front-end never runs it, since it passes its own
callbacks. Setting the gate_visibility option to FALSE keeps this board
eager too.
The same bundle carries a third channel, frozen, through which a
front-end reports the blocks whose inputs it has hidden (for example a
locked board that shows outputs but not controls). While frozen a block is
read-only: its expression, state readiness and the state it exposes for
serialization are held at the values last seen while editable, and the input
trigger is dropped, so a forged client input (which still fires the block's
own observer) reaches neither the expression, the block's status, a
re-evaluation, nor a save. Externally controllable inputs (see
external_ctrl_vars()) are held too – a high-priority observer reverts any
write while frozen – so not even the programmatic control channel can drive
a frozen block. Upstream data still flows through, and unfreezing resumes
normal input handling.
Evaluation trigger
block_eval_trigger() lets a block declare reactive state its evaluation
depends on that is not visible in the block expression or its data inputs
– the plot_block method returns the thematic and dark_mode board
options, so a theme change re-renders the plot. Its value joins the block's
unchanged-inputs check: the block re-evaluates when its interpolated
expression, its input data, or the value returned here changes. Reading a
reactive inside the method registers the dependency that wakes the block,
but it is the returned value changing – not the method being re-run –
that forces re-evaluation: returning a value equal to the previous one skips
it, even if the reactives it read invalidated. To force re-evaluation on an
event with no natural value, return a value that changes on it, such as an
incrementing counter. The default method returns NULL, declaring no such
dependency.