A call to board_server(), dispatched on objects inheriting from board,
returns a shiny::moduleServer(), containing all necessary logic to
manipulate board components via UI. Extensibility over currently available
functionality is provided in the form of S3, where a board_server()
implementation of board sub-classes may be provided, as well as via a
plugin architecture and callback functions which can be used to register
additional observers.
Usage
board_server(id, x, ...)
# S3 method for class 'board'
board_server(
id,
x,
plugins = board_plugins(x),
options = board_options(x),
callbacks = gate_stacks(),
callback_location = c("end", "start"),
...
)
eager(owner, blocks = character())
gate_stacks()Arguments
- id
Parent namespace
- x
Board
- ...
Generic consistency
- plugins
Board plugins as modules
- options
Board options (
NULLdefaults to the union of board, block and registry sourced options)- callbacks
Single (or list of) callback function(s) registering additional observers. A board is eager by default: it evaluates every block. A callback makes it lazy by returning
eager(owner, blocks), on its own or as one element of a list whose other elements are passed on to plugins as usual. Core then evaluates only the blocks some owner holds eager, and what feeds them; it seedsblocksas that owner's eager set before the first flush, and at most one callback may return one. Changes from then on travel as aneagercomponent under the same owner label through theupdatechannel each callback receives (see board_update and the Evaluation requests section). Each callback also receives avisibilitylist of the per-block channelsvisibleandfrozen, environments ofreactiveVals (core keeps one per board block as blocks are added and removed). Report whether a block is currently painted withvisibility$visible[[id]](TRUE)(orFALSEonce built but off screen, leavingNAuntil it is first built); the board renders a block only once it is reported painted, and holds background construction until every block the front-end holds eager is. Setvisibility$frozen[[id]](TRUE)to freeze a block's inputs (for example when its controls are hidden), so a forged input can no longer steer it.Core's own front-end drives these channels through a callback like any other:
gate_stacks()reads the stack accordion (seestack_ui()) and is the default, so a stacked board that renders core's UI is lazy, and a board that does not render it is left alone – it passes its own callbacks. A consumer that wants both keeps it in the list rather than replacing it –callbacks = list(gate_stacks(), my_callback), which is for a front-end that does render the accordion: on a stacked board it returns the blocks of the initially open stacks as its eager set, and reads that input only to refine it.- callback_location
Location of callback invocation (before or after plugins)
- owner
Label under which the front-end holds its blocks eager, as it would name itself in an
eagercomponent- blocks
Block IDs the front-end needs evaluated from the start
Value
A board_server() implementation (such as the default for the
board base class) is expected to return a shiny::moduleServer().
Active conditions
Conditions raised while blocks evaluate (errors, warnings and messages) are
exposed as a reactive data frame board$conditions on the read-only board
handed to plugins and callbacks, with one row per active condition and
columns block, phase, severity, message and id. It combines the
per-block server$conditions reactives (see block_server()), so a
consumer reads a single reactive — the whole board, or one block's frame
for fine-grained updates — rather than walking nested condition state. The
default notify_user() plugin renders its toasts from this source.
Evaluation requests
Deferred evaluation leaves a block that nothing currently needs holding its
last check — the status it reached, its result and the conditions it
reports (see block_server()). Anything that can reach the board_update()
channel can ask for such a block to be brought up to date, without putting it
on screen, through the evaluate and eager payload components. Both name
blocks, and core joins them, together with their upstream closure over
board_links() (without which they cannot produce a result), to the eval
set. They differ only in who lets go: an evaluate request is a one-off that
core drops once the block has run, while a block held eager stays evaluated
until its owner releases it.
Eager blocks are keyed by owner, the eager component mapping each owner to
a delta over the blocks it holds, so several consumers may hold the same
block and none of them overwrites another's set:
update(
list(
eager = set_names(
list(list(set = board_block_ids(board$board))),
session$ns("preview")
)
)
)A delta is set, add and rm, of which set states that owner's entire
set at once and cannot be combined with the other two. Releasing everything
is set = character(); releasing some blocks is rm, which — unlike set
and add — may name a block the board no longer has, so a release cannot be
rejected by a removal that raced it. Restating a set repairs a release that
never arrived, rather than letting it accumulate.
Core cannot infer the owner — the write and its effect are separated by a
flush — so the label travels in the payload. Nothing keys off shiny's
namespacing, but taking the label from session$ns() as above is what keeps
owners unique without a registry, and lets one module hold two independent
sets under two labels. An owner's set outlives the module that made it: core
drops a block from every set once it leaves the board, but an owner that
goes away without releasing holds what it held for the rest of the session.
Holding a block eager asks for evaluation and nothing else: nothing about
what is on screen changes. The front-end is an owner like any other – it
holds its blocks eager under the label it declared with eager() (see
block_server()) – so core never distinguishes its demand from any other
owner's, and a consumer holding a block eager cannot park what the
front-end is showing. Because these requests carry no state change, they are
also the one part of a payload a locked board still accepts.
Core drops a one-off request once the block has run — or has reported why it
cannot, such as an unconnected data input or a user input that was never set.
Either way the block has been checked against everything it depends on, so
once it leaves the eval set it goes on reporting what that check found, until
one of those changes (see block_server()). A request for a parked block
that is current is spent at once, since its status already reports what a
run would find, and requesting a block that is already in the eval set does
nothing.
Construction requests
Evaluation implies construction, but not the reverse: a consumer that needs a
block merely present — the code export reads each block's expression and
none of their results — had to make it run as well. The construct payload
component asks for construction on its own. Like evaluate it is a bare
character vector of block IDs, and the blocks it names are built in
dependency order and left unevaluated:
update(list(construct = board_block_ids(board$board)))Nothing is retained. Once a block is built it stays built, so unlike the two evaluation components there is no owner to name and nothing to hand back, and asking for a block that is already built does nothing. The request joins neither the eval set nor any owner's eager set, so it cannot turn a lazily evaluating board into an eagerly evaluating one.
A block that the same payload adds, or that an evaluate or eager names,
is already constructed — the add builds it directly, and evaluation demand
joins the needed set, which the background constructor builds. Pairing
construct with either is redundant rather than wrong. The component covers
what neither does: a block that must exist while nothing needs it evaluated.
The named blocks are built in the flush that applies the payload, which is
the work background_construction_delay otherwise paces out. A caller that
wants that pacing sends several smaller payloads rather than one.