Restoring a board no longer requires each object to come back with exactly
the classes saved for it, only as the same kind of object: the last saved
class with a blockr_deser() method of its own, such as block for any
block. A package can therefore rename or drop classes of its blocks without
breaking saved boards, where until now a block that lost a class it was saved
with failed to load (#379).
Board options carry a label, the name a UI can show an option by outside
its own ui, such as in a list of options. It is set by the label
argument of new_board_option(), which defaults to the ID in sentence case
("Data dir" for data_dir), stored like the category and read by
board_option_label(). Core's own option constructors take a label
argument too and name their input by it, so each name is written once. A
saved board keeps the labels of its options (#374).
The extra arguments of serve() for a board now reach blockr_app_ui() as
well as blockr_app_server(), so a board subclass can take an app-level
argument, such as a navbar, in both methods. Breaking for front-ends: a
blockr_app_ui() method that forwards its ... into the returned UI now
gets the arguments an app passes for the server as well, and has to tell
them from its own (#380).
The ...args argument of a variadic block's server is now a
reactives::reactive_exprs() collection rather than an internal class.
Reading an input with ...args[[1]] or ...args$x still returns its data,
and names() and length() read as before, while ...args[1] returns the
input's reactive and reactives::as_values(...args) the data of every input.
Breaking for a block server that calls as.list(...args), which now returns
the inputs' reactives rather than their data, or that writes a value into
...args, which is now an error (#361).
The trim_rv() export is gone. It removed a key from a reactiveValues()
object by reaching into shiny's internals, while a collection from the
reactives package, such as reactives::reactive_vals(), drops one with
x[key] <- NULL (#361).
Evaluation demand is now one multi-owner set rather than two channels. The
front-end's per-block required channel is gone: the blocks it needs
evaluated are held eager under an owner label, like any other consumer's.
Core no longer distinguishes a front-end's demand from a code export's, and
because no owner overwrites another's set, it cannot silently become
multi-writer the way required did. A board is eager by default and turns
lazy only when a front-end says so: its callback returns eager(owner, blocks), and core seeds blocks as that owner's eager set as it runs the
callbacks, before the first flush decides what to construct -- which no board
update can do, since a payload applies at the end of the flush it is written
in. Where no callback returns one the board stays eager, so a consumer
holding one block eager cannot park every other block. The visibility
bundle handed to callbacks keeps its per-block visible and frozen
channels, and reporting paint on visible is unchanged. Breaking for any
front-end that drives visibility$required (#321).
A block out of the eval set now reports what its last check found, and the
dormant status goes. It used to read dormant whether its last run still
held, failed, found it could not run, had been overtaken by an edit or had
never happened, so a consumer could not tell which parked blocks needed a
run. A check runs a needed block or finds that it cannot run, and records
what it read -- the block's expression, state and eval trigger, which blocks
feed its data inputs and what each of those held -- together with the status
it reached and the result it left. A parked block reads that status
(ready, failed, waiting or unset) while nothing the check read has
changed, stale once something has, and the new unevaluated without a
check, which also covers a block that is not built yet. Code that matches on
dormant has to read these instead. The upstream half of the old stale
check skipped any upstream that was itself parked, so a block lost its
stale flag as soon as the upstream that changed was parked again, and a
link removed from under it, or re-routed to another parked block, went
unnoticed (#362).
A parked block's result is now the one its last check left, where it used to
be NULL, so it goes with the status the block reports (#363).
The reason a block cannot run is now recorded by the check that finds it,
rather than when the block renders, so a block checked off screen by an
evaluate request carries it, and one fixed off screen drops it once it
runs (#362).
Links can now be placed rather than only appended. The modify_board_links()
arguments before and after, mirrored by links$before / links$after in
a board update payload, name where a link passed as add should sit, as
either the ID of the link to sit next to or its position among the links
into the same block. Board links are one flat vector interleaving every
target, so a board-wide index would name nothing a block can observe. Passing TRUE
instead places every added link against the whole set, so before = TRUE
prepends and after = TRUE appends, which is also the one spelling of
"first" that holds on a board with no links yet. Anchors resolve against the
links as they are before rm is applied, so a link can be placed relative to
one the same call removes. This matters for a variadic target,
whose ... argument order is the order of the links pointing at it: splicing
a block into an existing wire drops one link and adds two, and appending the
replacement moved the target's inputs out from under it, silently reordering
its rows. Holding the position previously meant reusing the removed link's
ID, so the inserted wire could not be given an ID of its own (#358).
Removing and editing links in one update no longer aborts on a link the same
update removes. The overlap between add and rm, which is how
apply_board_update() folds a links$mod delta in, was assigned in place
before the removals were applied, and that assignment validates the whole
link set -- so an edit claiming an input that a to-be-removed link still held
failed validation even though the end state was sound. Removals are now
applied first (#358).
Board files are now written and read by typedjson rather than
jsonlite::toJSON()/fromJSON(), so typed data stored in block state
survives the round trip. JSON has one number type, no typed NA and no
place for attributes, so a double came back an integer, an all-NA vector
lost its type, and names, Date and factor classes were dropped. Core's own
objects were unaffected because blockr_deser() rebuilds them through a
constructor that re-imposes the types the document could not carry, but
anything an extension stored in state has no such route back and arrived
changed. Both directions of the format change were measured: a document
written by the old pair reads back identically under the new one, and a
document written by the new one still reads under the old, so saved boards
keep loading either way (#351).
A link input equal to "Inf", "NA" or "NaN" now restores as the string
it was. The old reader decoded those tokens back to the numeric or logical
values they usually encode, so a contractually-character field came back
non-character and aborted the restore. Strings are now escaped where they
could be mistaken for a token, which removes the ambiguity rather than
papering over it field by field (#136).
The format.block() method now takes a state argument, so a caller
holding a block's live state can render that instead of the values it was
constructed with. The state section came from the constructor frame, which
is fixed at construction, so a block taken off a running board rendered its
load-time values however long ago a blocks$mod delta or an edit in the
block's own UI had moved them on. This mirrors blockr_ser.block(), which
already takes live state and falls back to the constructor scope when it is
absent. The section label states which of the two is shown, and the
default remains the constructor values (#352).
A block in a collapsed stack no longer evaluates once at load. Which stacks
render open is core's own decision, but it was left to bslib's default of
opening the first panel, so the board server knew nothing about what was on
screen until the accordion had reported -- and a board nothing has made lazy
is eager, so whatever got built in that window ran. The stack_ui() method
now states the open set explicitly and gate_stacks() declares it as the
board server is set up, leaving the client's report to refine that rather
than establish it. A board with no stacks binds no accordion input and stays
eager, as before (#343).
A parked block with no data inputs is now as quiescent as any other. The needed set reaches a block through its data reads, of which a source block has none, so anything reading its result -- the block card's summary, for one -- evaluated it while parked. Reading it now evaluates nothing, as for any parked block (#343).
Core's own board UI now drives visibility, through a board callback like any
other front-end rather than from inside the board server. Stacks render as an
accordion which opens one stack and collapses the rest, so on a stacked board
part of what is on screen was hidden from the first render while every block
evaluated and rendered regardless -- nothing read the input bslib had
already wired for reporting which stacks are open. The new gate_stacks()
callback holds the blocks of every open stack plus every unstacked block
eager and parks the rest, so collapsing a stack stops its blocks evaluating
and expanding one starts them again. Parked rather than dropped: a collapsed
stack's blocks stay built, so re-expanding shows them without a rebuild. It
is board_server()'s default callbacks value, so a board driven by another
front-end -- which passes its own callbacks, and whose UI never binds the
input -- is left alone; a consumer that wants both keeps it in the list,
callbacks = list(gate_stacks(), my_callback). Setting the gate_visibility
option to FALSE keeps the board eager, as it does for every other
front-end. The accordion container ID moves from <board>_stacks to the
board-namespaced <board>-stacks, which is what makes it readable from the
board module (#338).
Board updates gain a third request component, construct, a character vector
of block IDs to build without evaluating. Construction previously followed
evaluation as a side effect, so a consumer that needed a block merely present
-- the code export reads each block's expression and none of their results --
had to make it run as well, holding the whole board in the eval set for as
long as it needed the expressions. Unlike evaluate and eager it retains
no state: a built block stays built, so there is no owner to name and nothing
to release, and requesting 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 (#333).
Showing the generated code no longer writes the front-end's own demand
channel. A block the front-end had parked was overwritten and
never restored, so a single "Show code" turned a lazily evaluating board
into an eagerly evaluating one for the rest of the session, with nothing
left to release it. The export asks for construction alone through the
construct board update component, since the script is assembled from
block expressions and needs its blocks built rather than run: a board that
defers its off-screen blocks now stays deferred while the code is shown.
Export is held back by what a block reports about itself -- user inputs
that were never set, or an error from its last run -- rather than by eval
status, and the modal offers a one-off evaluation so that a block which has
never run can report either way. The generate_code plugin server takes
update in place of visibility, which is breaking for a front-end that
supplies its own generate_code_server() (#320).
apply_board_update() is now a real reducer rather than a no-op: its
default .board method applies the core delta (block, link and stack
mutations) to the supplied board and returns it, and update validation
now checks that applying the delta yields a valid board instead of
re-deriving the merged references. Extensions overriding
apply_board_update() must compose with NextMethod() to pick up the
core apply before layering their own payload slots (as blockr.dock does
for views). Breaking for front-ends that override the apply generic
(#311).
Block result previews now dispatch through a tabular display: an S3
object bundling the output container, render function, render trigger
and board options for a single result class, kept in sync by living on
one object. The active display is read from the blockr.tabular_display
option (via blockr_option()) and defaults to minimal_display, a
compact preview of the top n_rows rows (tibble-formatted when the
suggested tibble package is installed) that reflows to the width of its
panel. Set options(blockr.tabular_display = dt_display) to restore the
previous paginated, searchable DT table. Data, parser and transform
blocks render through whichever display is active; downstream packages
add their own by defining tabular_ui(), tabular_output(),
tabular_trigger() and tabular_options() methods on a tabular_display
sub-class and having users opt in via the option. Breaking for front-ends
that relied on the DT preview by default (#129).
DT moves from Imports to Suggests. It now backs only the opt-in
dt_display preview and the manage_links / manage_stacks reference
plugins -- which typical front-ends (e.g. blockr.dock) replace with their
own UI -- so a bare core install no longer pulls it in. Install DT
alongside if you use either (#129).
The default board plugin set (board_plugins()) no longer includes
the manage_links and manage_stacks editor plugins, which render
their tables through DT. Including them by default made a bare core
board fail with there is no package called 'DT' once DT became a
Suggest, breaking consumer test apps built on the default board. Add
them back with c(board_plugins(x), manage_links(), manage_stacks())
to restore interactive link and stack editing; both constructors now
raise a clear error when DT is missing. Breaking for front-ends that
relied on the core editors in the default board (#297).
A board's blockr_app_ui() and blockr_app_server() methods now receive the
request's parsed URL query parameters as a query argument, at both the GET
and the websocket phase. A board subclass reads it to make the rendered UI and
the server URL-aware from one source -- for example opening on the view named
by ?view= -- so the initial render and the server agree without a custom
loader. The default board methods ignore it. Breaking for front-ends: a
blockr_app_ui() / blockr_app_server() method that forwards its ... into
the returned UI must add a query formal, or the threaded argument lands in
... and renders as stray content (#291).
Cleanup of a removed block, stack or view now uses shiny's public
session$destroy(id) (requires shiny >= 1.14.0) rather than reaching into
undocumented shiny internals to tear down a module's inputs, outputs and
observers. The exported destroy_module() and the observe() trace hook
that captured per-module observers are removed; call session$destroy(id)
directly instead. Breaking (#202).
The board callback now gates block construction, evaluation and rendering
through per-block reactiveVal channels it receives as visibility --
required (which blocks are needed) and visible (which are on screen) --
in place of the single visible write-channel. Breaking for front-ends.
The visible channel is now logical, mirroring required: a front-end
writes TRUE once a block is painted, FALSE once it is built but off
screen, and leaves NA until it is first built (it previously carried the
rendered view id, or NA_character_ off screen). The extra state lets a
front-end distinguish "never built" from "built, off screen" on this one
channel, so it no longer has to reset the slot to unbuilt when a card
leaves the screen -- which would erase the "was built" fact the render gate
needs. is_visible() is accordingly isTRUE() rather than !is.na().
Breaking for front-ends that wrote a view id (#306).
The visibility bundle carries a third channel, frozen, letting a
front-end freeze a block's inputs server-side: setting
visibility$frozen[[id]](TRUE) -- for example for a locked board that shows
outputs but hides controls -- holds the block's expression, state readiness
and serialized state at their last editable values and drops the input
trigger, so a forged Shiny.setInputValue (which still fires the block's own
observer) reaches neither the expression, the block's status, a
re-evaluation, nor a saved board. Externally controllable inputs are held too
-- a high-priority observer reverts any write while frozen -- so a frozen
block is fully read-only. Upstream-data-driven re-evaluation still runs, and
unfreezing resumes normal input handling (#231).
background_construction_delay now accepts Inf, skipping the background
construction pass so a block is built only once it becomes required. Code
export ("Show code") then asks for every block on the board to be built, so
the exported script covers the whole board; an off-screen block that is not
fully configured holds the export back instead of emitting broken code
(#269).
Code export gates on the set of blocks that actually carry an expression,
not on eval status alone, so a board with unbuilt blocks can no longer emit
a script that assigns to a variable named NA. "Show code" always opens the
modal, which reports "Preparing code..." while the board materializes and a
not-ready note when a block is left unconfigured, rather than silently
producing nothing (#300).
blockr_ser() accepts a partial block-state snapshot: a board block omitted
from blocks (or mapped to NULL) serializes from its constructor scope
instead of aborting with length(blocks) == length(x). Saving a board under
deferred construction, where off-screen blocks are never built and carry no
live state, no longer fails -- unbuilt blocks round-trip from their
constructors rather than being dropped (#279).
With a finite background_construction_delay, the staggered builder now
prioritizes the on-screen view: each tick builds the next block needed by the
visible set before the rest of the backlog, so switching view re-prioritizes
construction toward what is now on screen and the newly-visible blocks come up
progressively instead of in one blocking build (#275).
The staggered builder no longer monopolizes the event loop while it runs. Each tick's pacing delay now begins once the just-built block has flushed rather than while its reactive graph is still flushing, so pending user input is serviced within one tick instead of behind the entire backlog (#276).
With a finite background_construction_delay (the default), a downstream
block's data input no longer latches at NULL when its input reactive runs
before the upstream is built. The input read the upstream server under
isolate() and never re-resolved once that server registered a moment
later; it now re-fires when the upstream is constructed, so the block picks
up its data instead of starving for the rest of the session (#298).
Captured block conditions are no longer glue-interpolated when logged, so a
block whose warning or error text contains braces -- e.g. the {summary_fun}
/ {data} placeholders in tidyr::pivot_wider()'s duplicate-value warning --
no longer aborts the reactive with "Failed to evaluate glue component". This
extends the notify() toast path's use_glue = FALSE treatment to the
capture_conditions() handlers and the replay() methods (#268).
Switching the active panel or view no longer re-evaluates blocks whose needed
status is unchanged. Each block gates its data inputs and eval status on its
own per-block needed slot rather than the whole needed set, and skips
re-evaluation when its interpolated expression and input data are unchanged,
so switching panel or view re-evaluates only the newly-visible block, not the
entire shared upstream pipeline (#271).
Board deserialization can degrade gracefully instead of aborting the whole
load when a block cannot be restored -- its constructor or the providing
package is unavailable, its payload cannot be reconstructed, or the
round-trip class check fails. blockr_deser() for blocks gains an
on_error argument ("abort" or "drop"), defaulting to
blockr_option("deser_on_error", "abort") so a deployment can opt into
dropping offending blocks (with a warning) via
options(blockr.deser_on_error = "drop") or the BLOCKR_DESER_ON_ERROR
environment variable. Links and stacks referencing a dropped block are
pruned so the surrounding board still loads (#264).
A links$mod board update that changes a link to a different value (for
example switching a merge_block's input from x to y) now applies
instead of being silently discarded. Folding the modified link into the
add accumulator with base c() dropped the links class when the
accumulator was empty, so the downstream link setup ran without its
to/input arguments and aborted with missing subscript, rolling back
the whole update; it now concatenates with vec_c() (#287).
A req() -- or any silent flow-control throw with an empty message --
evaluated while a block's conditions are captured is no longer recorded as
a block error, so a block that is merely not currently needed no longer
flashes a text-less red error band (and a false error count) in deployed
apps. Empty-message conditions are filtered by emptiness rather than class,
so a validate(need(x, "msg")) message still surfaces (#289).
The structured argument-spec API is renamed to a block-neutral stem, so a
non-block consumer -- an extension documenting its externally controllable
variables -- no longer reads as describing a block. new_block_arg() /
new_block_args() become new_arg_spec() / new_arg_specs() (classes
arg_spec / arg_specs), and the block_arg_description() /
block_arg_example() / block_arg_type() getters become
arg_spec_description() / arg_spec_example() / arg_spec_type(). The
arg_*() type constructors, register_block()'s arguments argument and the
block_meta_arguments() accessor keep their names, being genuinely about a
block's arguments. The old names remain as deprecated wrappers that warn once
and forward to the new ones, so existing
register_block(arguments = new_block_args(...)) call sites keep working;
migrate them to the arg_spec family (#295).
A block constructed for a class with no registry entry is now imputed a
class-derived default metadata record at construction (name from the class,
default category and icon) instead of being left without one. It still warns,
but the block is then self-describing: block_metadata() and the
block_meta_*() accessors report those defaults rather than a metadata read
aborting -- so a cosmetic lookup can no longer take down a board whose
registry has been curated (#299).
Blocks now carry an eval status stale. A parked block (built but not
currently needed, so not evaluating) whose upstream has produced a new result
since it last evaluated reports stale, flagging that its last-known result
is out of date without forcing a re-evaluation. A front-end can render it
distinctly (e.g. a muted node badge); previously such a block read dormant
like an up-to-date one, so a break introduced upstream stayed hidden until
the block was visited (#310).
Board updates gain two request components, evaluate and eager, for
evaluating a parked block without making it visible. Both name blocks that
are joined, with their upstream closure, to the eval set so they publish a
current result and current conditions; core drops an evaluate request once
the block has run, while a block held eager stays evaluated until released.
Eager sets are keyed by owner (list(<owner> = list(set =, add =, rm =)),
conventionally labeled session$ns("...")), so two consumers may hold the
same block without either releasing the other's. Previously the only lever
was the front-end's required channel, which extensions never receive and
which latches the block into the eval set, so a consumer had no way to tell
whether a change it had just made broke an off-screen block. Since they carry
no state change, request components are also the one part of a payload a
locked board still accepts, and a payload rejected for being locked now
records an outcome in board$last_update instead of being dropped silently
(#318).
The bbquote() walk no longer drops NULL elements from a call. Assigning
the recursive step's result with [[<- deleted the element whenever it was
NULL, leaving the call shorter than its names and aborting with an 'names' attribute [4] must be the same length as the vector [3] error (or, for a
named NULL, subscript out of bounds). This hit any expression carrying a
literal NULL argument, and -- because a function definition's srcref slot
is NULL under keep.source = FALSE -- any expression defining a function,
but only once the package was installed rather than loaded with load_all()
(#323).
background_construction_delay, default 50 ms; 0 opts out).dormant, waiting, unset, failed,
ready) that gates evaluation, rendering and downstream data, so a block
never evaluates against missing inputs or shows a stale result.generate_code()) now waits until every block is settled,
showing a "board not ready" note instead of a partial script.server$conditions and board$conditions(); the block server no longer
returns its raw cond object.notify() helper gains glue and log arguments for literal
(brace-safe) text and skipping redundant logging.allow_empty_state argument of new_block() now accepts a structured
list(input = ..., data = ...) form to relax required inputs and the
variadic minimum per input kind.loader
argument to serve() (default local_loader()); this drops the
process-global staging slot, so get_serve_obj() and restore_board()'s
meta argument are gone.page_size, n_rows, filter_rows) are now saved and restored, not reset
to defaults on reload.blockr.locked
option, which refuses every mutation while set.@block, @blockArg, ...), collected by the new
block_registration_roclet(); extension packages register via the exported
register_package_blocks().new_block_args(),
new_block_arg()) with JSON-Schema-subset type descriptors (arg_string(),
arg_enum(), ...), read via block_metadata(); registry_metadata() is
deprecated.board_blocks(), board_links(), board_stacks()) are now
pure reads, removing quadratic re-validation that dominated large-board
startup.external_ctrl_vars() and predicate
has_external_ctrl() expose a component's externally controllable variables.trim_rv() fully removes entries from a reactiveValues object
(assigning NULL leaves the key behind); unlinking a variadic argument now
drops it outright.str_value() compact printer, with matching utils::str() methods, now
covers all remaining domain classes and containers.blockr_deser.list() method now forwards ... to per-class
deserializers, letting callers thread context to nested deserializers.mod slots in update(...) payloads for blocks, links and
stacks now uniformly expect a delta shape: a named list keyed
by entry ID, where each entry is a named list of argument values to
apply on top of the live entry.
blocks, keys must be in block_external_ctrl_vars(blk) —
non-ctrl-able changes go through rm + add. Ctrl-arg writes hit
the corresponding reactiveVal in place; block_name (always
treated as ctrl-able) updates the block's registry attribute.links and stacks, deltas are merged onto the current entry
via the new update_link() / update_stack() S3 generics. The
default methods reconstruct the entry through its stored
constructor, preserving sub-class attributes. Sub-class owners
(e.g. dock_stack adding color) only need to register a method
when their constructor deviates from the convention (#175).block_external_ctrl_vars() always includes "block_name" — every
block can be renamed through update(...) regardless of its
external_ctrl opt-in. The default ctrl plugin panel renders a
block_name field on every block card alongside any opted-in ctrl
vars. block_supports_external_ctrl() (which would now be TRUE for
every block) has been removed; the gates that used to call it have
been simplified accordingly.block_server.block now constructs a block_name
reactiveVal per block and appends it to the vars list passed to the
ctrl plugin. Two guarded observers keep that reactiveVal in sync with
the block's block_name attribute on the board (one emits an
update(mod = ...) when the ctrl plugin writes the rv; the other
pulls registry-attr changes back into the rv). The ctrl_block_server
plugin signature is unchanged — block authors and custom ctrl plugins
see uniform vars reactiveVals.attach_default_packages can be set to opt into evaluating
block expressions with objects from default packages directly available.ctrl_block() plugin for external block control, allowing blocks to be
driven programmatically from outside the standard block UI.clear_board() for removing all blocks/stacks from a board.bbquote() and helpers (., ..) for cleaner code generation via
bquote()-based quasiquotation.custom_plugins() and custom_options() for easier board
customization.block_metadata() for retrieving per-block metadata and attach block
metadata with defaults to block objects.blockr_test_exports(), new_mock_session(),
export_safely()) for use in downstream package tests.expr server return value
component cond.get_board_option_value() to make available current option settings
via session$UserData.toolbar_ui() which takes case of the "core" toolbar UI component.chr_ply() and related, as well as miscellaneous utilities
such as set_names(), coal(), etc. are now exported for use in dependent
packages.export_code() to make it easier for third-party generate_code()
plugin implementations.evaluate::evaluate() to capture plots.new_fixed_block() for applying a fixed (i.e. non-parametrized)
transformation to data input.session.is_string(), is_count(), etc.block_render_trigger() to control per block class when to
re-render the block output.reactiveValues() object
containing notification types a separate components.session$reload().